<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://www.pragmaticcoding.ca/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.pragmaticcoding.ca/" rel="alternate" type="text/html" /><updated>2026-07-11T08:43:48+00:00</updated><id>https://www.pragmaticcoding.ca/feed.xml</id><title type="html">PragmaticCoding</title><subtitle>PragmaticCoding is all about ... pragmatic coding!  Writing code that gets the job done and is easy to read, understand, maintain and enhance.</subtitle><author><name>Dave Barrett</name><email>pragmaticcoding8@gmail.com</email></author><entry><title type="html">You Need a Public Domain</title><link href="https://www.pragmaticcoding.ca/homelab/public-domain" rel="alternate" type="text/html" title="You Need a Public Domain" /><published>2026-06-24T17:00:00+00:00</published><updated>2026-06-24T17:00:00+00:00</updated><id>https://www.pragmaticcoding.ca/homelab/public-domain</id><content type="html" xml:base="https://www.pragmaticcoding.ca/homelab/public-domain"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>As someone who has already gone through the process of registering and managing a domain for my public website, specifically “pragmaticcoding.ca”, it was a fairly trivial step to register one for personal use.  I already had a GoDaddy account from years and years ago, so it was just a matter of finding a domain name and charging it to my account.</p>

<p>But why would you want to do this?  How can it help with self-hosting?  Let’s take a look…</p>

<h1 id="what-is-a-public-domain">What is a Public Domain?</h1>

<p>A “public domain” (my term, since I couldn’t find a better one anywhere on the web) is any domain that is registered into the system of domains that are managed by the Internet Corporation for Assigned Names and Numbers (ICANN).</p>

<p>Everyone is familiar with the “top level” domains.  These are things like <code class="language-plaintext highlighter-rouge">.com</code> and <code class="language-plaintext highlighter-rouge">.net</code> and <code class="language-plaintext highlighter-rouge">.uk</code> or <code class="language-plaintext highlighter-rouge">.ca</code>.  There are also some subdomains of these that you see quite often.  You might have seen British domains that are part of <code class="language-plaintext highlighter-rouge">.co.uk</code>, because in the UK, all of the commercial sites have been forced into <code class="language-plaintext highlighter-rouge">.co.uk</code> domain.  In Canada you might see a <code class="language-plaintext highlighter-rouge">.on.ca</code> or a <code class="language-plaintext highlighter-rouge">.bc.ca</code> which are domains for Ontario and British Columbia.  Back in the early days of the internet, to get a <code class="language-plaintext highlighter-rouge">.ca</code> domain, you needed to prove that you did business across several provinces.  Otherwise they told you to register with your provincial domain.  They don’t do that any more.</p>

<p>If your domain ends with any of the ICANN top level domains, then it’s what I would call a “public domain”.</p>

<p>And you should have one.</p>

<h2 id="registering-a-domain">Registering a Domain</h2>

<p>Each of these higher level domains maintain a registrar.  If you want to get a subdomain of one of these domains, you must put in a request with the registrar.  In practice, nobody deals directly with the registrars.  Instead you deal with a company that acts as an agent (or maybe a delegate of a registrar), and you pay through them.  These are companies like GoDaddy, and NameCheap.  In Canada there is one called “Register.ca”, which isn’t the <code class="language-plaintext highlighter-rouge">.ca</code> registrar, but just another agent.  You can register domains in <code class="language-plaintext highlighter-rouge">.net</code>, <code class="language-plaintext highlighter-rouge">.org</code>, <code class="language-plaintext highlighter-rouge">.com</code> as well as <code class="language-plaintext highlighter-rouge">.ca</code> through “Register.ca”.</p>

<p>Who you pick is up to you.  It literally doesn’t matter which one you pick, but it would probably be best if you pick one that’s likely to be around for a few years.  This is because the future management of your domain is going to be connected to your account at that agent, and tranferring it might be problematic if they go out of business with little warning.  Most of these companies also provide a range of other services like email and web hosting.  Prices for the same domain may vary between agents, too.</p>

<p>Personally, I use GoDaddy.  This is mostly because I was involved in a commercial operation many years ago that required me to set up an account with them.  I still have the account, and it was easy just to use it.  I’m not sure that registrations with GoDaddy are the cheapest, but they’ll probably be around for a while.</p>

<p>Depending on the domain name, and the top level domain that you register with, you can expect to pay from $15 to $30 a year for a domain for your self-hosting.</p>

<h2 id="domain-name-services">Domain Name Services</h2>

<p>The important thing to remember about this is that all of that registration stuff is just about the <em>names</em>.  But these names are important because they provide the structure to allow everyone on the Internet to find the actual addresses of services they are looking for.  This facility is provided by “Domain Name Servers”, often just called DNS servers.</p>

<p>The one thing that every public domain must have is a DNS server that can service public requests for information about the domain.  For the most part, the registrar of the parent domain that holds your domain will have some level of DNS services for your domain, although that may just redirect to the agent through which you registered the domain.</p>

<p>Generally speaking, information about your domain is stored in something called a “Zone File”.  A “zone” is usually just a domain.  The kind of information held in a zone file would be the addresses of the DNS servers that it uses, the addresses of the email servers that it uses and address of web servers, or application servers that it controls.</p>

<p>This is where the public part becomes very important.  Anything that you put into your zone file can be seen by <strong>anyone</strong> in the internet.  This is important for things like email handling, because you want email servers anywhere in the world to know where to connect to deliver email to your domain.</p>

<p>This public visibility is why you need a public domain.</p>

<h1 id="email-handling">Email Handling</h1>

<p>Years and years and years ago, my wife and I got our first email addresses from our ISP, which was Bell Canada, and they had a service called “Sympatico”.  This meant that we had <code class="language-plaintext highlighter-rouge">name@sympatico.ca</code> addresses.  Over time, the “Sympatico” service disappeared, but those email addresses continued to work as part of the Bell service.  However, we had migrated to Gmail 20 years ago, so those <code class="language-plaintext highlighter-rouge">sympatico.ca</code> addresses were largely unused and forgotten over time.</p>

<p>If you live in Canada, then you probably know that Canada is especially poorly served with both cell phone and internet services.  There’s a tiny handful of providers, they’re ridiculously expensive, and the service is low quality.</p>

<p>About a year ago, I finally became exhausted with the cost of my internet and TV service from Bell when the monthly bill increased by yet another $5 a month.  Also, Bell is the fibre provider for most of Canada, and in my neighbourhood, which is part of a large metropolitan suburban area, they haven’t bothered to install fibre to the home.  This means the fastest speed possible was a paultry 25Mbps.</p>

<p>As a result, I cancelled my fibre connection, held my nose, and signed up with the only alternative which was cable internet.</p>

<p>I didn’t really think to much about it, but it was clear to me that as soon as I cancelled the service with Bell, those <code class="language-plaintext highlighter-rouge">sympatico.ca</code> address were going to cease to function.</p>

<p>I’m suprised at how quickly the fallout happened.  Just a couple of days, in fact.</p>

<p>My wife asked me, “Did anything strange happen with email, I’m not getting email updates from Expedia?”.</p>

<p>It seems that her Expedia account was so old that it still used her <code class="language-plaintext highlighter-rouge">sympatico.ca</code> address.  Oops! You cannot change the email address on an account without having access to the old account because they send it a confirmation email that you have to acknowledge.  We had some accumulated reward points that we didn’t want to lose, and Expedia’s Customer Service told us that she’d have to create a new account and then they could transfer the points over when it was set up.  So that’s what we did.</p>

<p>I’m sure that lots of you have email addresses provided from your ISP’s.  I’m sure that you also have lots of accounts with various websites that depend on those email addresses.</p>

<p>In a way, you’re locked in with those ISP’s unless you’re willing to go through the pain and suffering of updating all of your accounts on every website that you use.  And remember, you have to do this <strong>before</strong> you disconnect the ISP address.  This could be a real bother if you’re moving and don’t have the option to keep your old ISP.</p>

<p>You could one of the other services, like gmail.com or hotmail.com, but then you’re tied in to those.  What if, like me, you’re fed up with Gmail?  They were cool back in the early, “Do no evil”, days.  But those days are long gone.</p>

<p>Having a public domain solves that problem.  It also means that for as long as you own that domain, you’ll never have to change your email address ever again.  You can have that email address for life.</p>

<p>It also has one other key benefit:  There’s no doubt that having your own domain-based email address is much more appealing than having whatever email address you could find on a public service.  Think on the simplicity of <code class="language-plaintext highlighter-rouge">george@thesmiths.org</code> over <code class="language-plaintext highlighter-rouge">georgesmith87302@hotmail.com</code>.</p>

<p>There are two approaches you can take to achieve this.  But first, one idea you should not pursue…</p>

<h2 id="dont-manage-your-own-email-server">Don’t Manage Your Own eMail Server</h2>

<p>For decades, I was involved with managing the email server for the company I worked for.  In the beginning, it was really the only option, because hosted email services from providers like Google and Microsoft simply didn’t exist - especially in the corporate space.  We installed a Lotus Notes server back in the 1990’s, and it was comparitively easy.  Just set up the DNS to point to it and configure the Lotus Notes server and turn it on.</p>

<p>Over the years, it became more and more complicated to manage this server.  The big problem was spam - more specifically, the measures that the rest of the world were using to combat spam.  This meant that we had to be very careful to make sure that we didn’t get blacklisted in the spam filters, and I can tell you this was a shock the first time that it happened.</p>

<p>In some ways we were lucky that we had been running our own server for years, because we had established ourselves as a legitimate source which avoided a lot of hassles.</p>

<p>Technically, it’s not difficult to set up and run an email server in your homelab today.  It’s not like you’ll have to navigate the archane complexity of <code class="language-plaintext highlighter-rouge">sendmail</code> or anything like it.  On the other hand, all the reports I’ve seen say that you’ll spend way too much time chasing around delivery problems caused by anit-spam measures implemented by your email recipient’s services.</p>

<p>Finally, the other big downside to hosting your own mail server in your homelab is that, by definition, it has to be directly accessible from the Internet at large.  This means that you absolutely have to open up a hole in your firewall to allow SMTP connections to come in from anywhere in the Internet.  Not through a VPN, straight in.  With care, this is something that you <em>can</em> do, but for me this is something that I’d <em>rather not</em> do.</p>

<p>Just let somebody else handle the headache of running an email server.</p>

<h2 id="mail-redirect">Mail Redirect</h2>

<p>This is a good alternative if you aren’t ready to ditch your Gmail account (or even your ISP’s email) but you just want to be able to use addresses from your own domain.  You can set it up for free, and it works fairly reliably.</p>

<p>One caveat to this approach is that not all of the mail services that you might currently be using support the outbound redirection scheme described below.  If this is the case with your mail service provider, you’ll have to go with the second solution that I describe.</p>

<h3 id="incoming-mail">Incoming Mail</h3>

<p>In order to have mail sent to <code class="language-plaintext highlighter-rouge">somebody@yourdomain.org</code> you’ll need to use an “Email Forwading” service.  Your best bet here is to simply do a search for “email forwarding service” and find one that appeals to you.  For what it’s worth, I’m using <code class="language-plaintext highlighter-rouge">improvMX.com</code> and it seems to work fine.  It’s free, too - at least for forwarding just two addresses.</p>

<p>You’ll need to set up an account with one of these services and then use whatever interface they give you to set up “aliases” for the email addresses that you want to forward.  The alias is the address that you want have people send to, and the forwarded address is the actual address in you email service that will receive the email.  For instance you might set up <code class="language-plaintext highlighter-rouge">somebody@yourdomain.org</code> to forward to <code class="language-plaintext highlighter-rouge">georgesmith9983203@gmail.com</code>.</p>

<p>The last step is to set up your domain to enable the email forwarding and to specify the forwarding service as the mail handler for your domain.  This involves adding entries to the zone file for you domain in your public DNS server.</p>

<p>Let’s look at the how to direct email to your forwarding service…</p>

<p>This is done via <code class="language-plaintext highlighter-rouge">MX</code> entries in your zone file.  <code class="language-plaintext highlighter-rouge">MX</code> stands for “Mail Exchange” and each <code class="language-plaintext highlighter-rouge">MX</code> entry lists the URL of a mail server for your domain.  You can also specify a priority for each mail server.</p>

<p>Somewhere in the documentation or setup pages for the forwarding service, you’ll find information about how to set up your domain.  This will include the URL’s of the mail servers that you should use.  For each one, you create an MX entry in your zone file.  The service I’m using specifies <code class="language-plaintext highlighter-rouge">mx1.improvmx.com</code> and <code class="language-plaintext highlighter-rouge">mx2.improve.com</code> with the first one having a priority of <code class="language-plaintext highlighter-rouge">10</code> and the second one with a priority of <code class="language-plaintext highlighter-rouge">20</code>.</p>

<p>How you enter the addresses and priorities specified by the forwarding service you select will depend on the UI provided by your DNS service.</p>

<p>Finally, you’ll need to set up something in your DNS zone that tells the forwarding service that they are authorized to perform the forwarding.  At least, this is what ImprovMX.com wants, and I expect that others will too.  For ImprovMX.com, they want a <code class="language-plaintext highlighter-rouge">TXT</code> entry with a specific string.</p>

<p>A <code class="language-plaintext highlighter-rouge">TXT</code> entry in your zone file is just text string that anybody in the Internet can read.  The important thing is that nobody else can <em>write</em> entries in your zone file except you.  So if the <code class="language-plaintext highlighter-rouge">TXT</code> entry that they are looking for is in your zone file, then <em>you</em> had to have put it there.  For my ImproveMX.com account, the string is “v=spf1 include:spf.improvmx.com ~all”.  Presumably, this isn’t just authorization, but some sort of configuration as well.</p>

<h3 id="outgoing-mail">Outgoing Mail</h3>

<p>Not all email service providers support alias accounts for outgoing emails.  Gmail does, and I strongly suspect that all of the other big, big providers do as well.</p>

<p>In order to do this, however, you are going to need an SMTP relay service.  I’ve been using Smtp2Go after having issues with Brevo, but there are quite a few free services available.  Once again, do an Internet search for “smtp relay service” and you’ll quickly get lists of services that you can use.  All of these free services have limits on how many emails you can send in a time period, typically daily or monthly.  Smtp2Go, is pretty restrictive at 1,000/month, but even that is way more than I’ll ever need.</p>

<p>Whatever service that you pick, it’s almost certainly going to require that you create an account.  Then, of course, it will need you to prove that you own the domain from which you are going to be sending emails.  That’s going to involve creating some specific entries in your DNS zone file.  Smtp2Go uses 3 <code class="language-plaintext highlighter-rouge">CNAME</code> entries, and they give you the exact information with buttons to copy the data so that you can paste it into the GUI for your DNS server.</p>

<p>The last prepratory step is to create a user/password that can send emails through the SMTP service.  These are the credentials that you’ll be putting into Gmail.</p>

<p>Now you’re ready to set up the outgoing alias on Gmail.  Go into the web interface for you Gmail account at <code class="language-plaintext highlighter-rouge">https://mail.google.com</code> and log in.  Then click the gear icon, which should bring up a settings sidebar, which should have an link called “See all settings”.  Click on that link, and you’ll get a settings screen that has about 10 tabs at the top.  Click on the “Accounts and Import” tab, and you’ll then see a section that says “Send mail as:”.  Then you can click on the “Add another email address” link, which should bring up a pop-up window that allows you to enter the details of the new address.</p>

<p>Give this address an name and then enter the address that you’re using from your own domain, for instance <code class="language-plaintext highlighter-rouge">somebody@mydomain.org</code>.  Make sure the “Treat as an alias” box is checked off.</p>

<p>In the next step you enter the URL and port of the SMTP server, the user id and the password.  Then save it and you should be good to go.  Use “Compose” to send a test email to yourself.</p>

<h2 id="mail-service-that-uses-your-domain">Mail Service that Uses Your Domain</h2>

<p>My migration path plan is to move my email handling from Gmail and over to a Canadian company called NorthMail (northmail.ca).  They are new, and I’ve contacted them a few times with some compatibility issues that I encountered, and they’ve always indicated that what I wanted was in their future plans.</p>

<p>The first thing I asked them about was compatibility with FairEmail, which is the email app I use on my phone.  They now support it.  However, they didn’t support the mail forwarding scheme described above for outgoing mail, although they indicated that they had requests for it.</p>

<p>I checked back last month, and they now have a “For Business” product which allows you to set up accounts within your own domain.  It will work just as well for families with their own public domains as for a business, and the price per mailbox is about the same as for an individual mailbox.  This is the direction that I’m planning to go, although it won’t be transparent to my wife as she’s currently using the Gmail app for email on her phone.  I’m going to flip her over to FairEmail first.  This means it will have to be a process.</p>

<p>Setting this up is going to be easy for me.  Just purchase the account with NorthMail and tell them what my domain name and email addresses will be, and then configure my public DNS service such that my MX entries point to the NorthMail servers instead of the ImprovMX servers.</p>

<p>I’m sure that you can find similar services from thousands of other providers around the world.</p>

<h1 id="ssl-certificates">SSL Certificates</h1>

<p>Another story from the deep past…</p>

<p>Back around 1997 the company that I worked for needed to register a domain and get an SSL certificate for our web server.  Back in those days you dealt directly with the registrar, and we had to prove that we did business across the country before we could register directly into the <code class="language-plaintext highlighter-rouge">.ca</code> domain.  We also dealt directly with VeriSign to get the certificate.</p>

<p>You could not get a certicate from VeriSign unless you could prove that you owned the website and the business associated with it.  I remember having to go get a copy of our articles of incorporation to provide this proof.  In truth, I cannot remember 100% if we needed them for the domain or for the certificate, but I feel like it was more likely to have been the certificate.  It <em>was</em> 30 years ago, though.</p>

<p>I do remember that the whole process was an ordeal that literally took weeks to complete.</p>

<p>Nowadays, it’s not even remotely difficult.  But you should remember that an SSL certificate does 2 things:</p>

<ol>
  <li>Proves that web server that you are connecting to is actually the one in your address bar.</li>
  <li>Provides the keys to allow your browser to use encrypted communication with the web server.</li>
</ol>

<p>Both of these are important, but it’s the first one which provides the technical hurdles to getting a certificate.</p>

<p>First off, certificates are issued by only a few <strong>trusted</strong> certificate authorities.  Your browser is going to have a list of certificate authorities that it trusts.  If it encounters a website with a certificate that is not signed one of these trusted authorities, it will display ugly warning messages.  This includes the “self-signed” certificates that some of your homelab servers may use by default.</p>

<p>This means that you need to get your certificates from one of these trusted authorities.  Today, we have a system called “ACME”, which stands for “Automatic Certificate Management Environment”, that you can use to automate the process of getting a certificate from a trusted authority.</p>

<p>Just like back in the 1990’s, the key element that you have to prove to the certificate authority is that you own and control the web server and the domain for which you are trying to acquire a certificate.  There are two ways to do this:</p>

<ol>
  <li>Make a specific customization to the web server that the certificate authority can connect to and detect.</li>
  <li>Make a specific customization to the zone file for the domain that the certificate authority can detect.</li>
</ol>

<p>Item (1) is problematic.  Many of the servers for which you’ll want to issue certificates are not going to be publicly accessible, and therefore cannot be connected to by the certificate authority.</p>

<p>Item (2) is trivial if you have a public domain.</p>

<p>And remember, certificates expire.  And proper certificates expire every few months.  So this is something that you want to set up and then forget about.  You’re not going to want to set up some hole in your firewall so that certificates can be re-issued at any random time.</p>

<h1 id="dynamic-domain-name-system">Dynamic Domain Name System</h1>

<p>You are probably aware that if you are using a normal residential internet connection, then your public IP address will change from time to time.  Anything that you try to set up that relies on using the public IP address of your connection will need to make sure that it has the current IP address in order to work.</p>

<p>You can use a service like “Duck DNS” to do this.  That’s going to require that you create an entry in their zone file, and you’ll end up with a hostname like ‘fred.duckdns.org’.  Then you’ll need to set up a script that checks your connection’s IP address and updates the DuckDNS.org zone file with the new address.</p>

<p>This works, but you’re tied into yet another service that you’ve got to keep track of.</p>

<p>You can do <em>exactly</em> the same thing with a public domain.  In fact, if you are using OPNSense, this is just another service that you can set up in your firewall and it will run on a scheduled basis and keep your zone file up to date with the current address of your IP connection.</p>

<p>This is called “Dynamic DNS” (or “DDNS”), because your DNS zone is dynamically adjusted to reflect changes in the IP addresses of the hostnames.</p>

<p>This is really useful if you are setting up an incoming VPN, because you’ll need to let the roaming clients know where to connect to…</p>

<h1 id="integration-with-vpns">Integration with VPNs</h1>

<p>This refers to VPN’s that you set up to allow access into your network from outside.  You might be familiar with using a VPN as a client, where you connect into a commercial VPN in order to anonymize your traffic, enter the Internet from another country, or to prevent your ISP from snooping on your connections.  Generally speaking, at some point the clients for these services are going to need to connect to a specific gateway or group of gateways in order to join the VPN.</p>

<p>The same situation exists when you create a VPN for an outside client (or peer) to access <em>your</em> network.  You’ll need a publicly accessible gateway that the clients can connect to.  But you cannot give them an IP address, because your ISP could change it at any given time.  This is where DDNS comes in to play.</p>

<h2 id="tailscale">Tailscale</h2>

<p>The other implementation of VPN’s that you are likely to use is “TailScale”.  The TailScale service actually maintains its own DNS service for your TailScale network that only your TailScale clients can access.  This is great, and allows you to use URL’s instead of IP addresses when you’re setting up clients.</p>

<p>However, if you are like me and you don’t want to go through TailScale when you’re on your own WiFi this isn’t going to work all by itself.  You’ll want to connect directly to the server from your WiFi, and when you leave your house you’ll want to connect through TailScale.</p>

<p>If you have a public DNS server and a private internal DNS server (which you probably should have), then you can set up a situation where your internal DNS server has entries that point directly to the internal addresses of your servers, and the public DNS server contains the TailScale addresses.</p>

<p>When you are on your WiFi, you’ll be using your internal DNS server and therefore connect directly to your services without TailScale.  When you are outside your house, you’ll be using your cell provider’s DNS server, which will get your addresses from your public DNS server and route you through TailScale to your severs.  This is called “Split Horizen DNS”.</p>

<h1 id="api-tokens">API Tokens</h1>

<p>Some of the things I described require that you are able to update your DNS zone file in an automated fashion.  This is usually accomplished through API tokens, which provide a way for applications to connect to your DNS service and make changes to your zone file.  Essentially, they are fairly long strings of gibberish which are unique to your account with the DNS service.</p>

<p>Generally, you can create multiple API tokens.  This allows you to have different tokens for different purposes, and you can usually disable them, or have them expire, or delete them whenever you want.</p>

<p>Not all DNS services support API tokens, so you’ll have to be on the lookout for this when you pick a DNS service.  GoDaddy, for instance, only supports API tokens for what they call “premium” clients - those clients with 50 or more domains registered through them.  I think this is a change in the past few years.</p>

<p>You don’t have to pick your registrar agent with this in mind, though.  It’s relatively trivial to pass the handling of your DNS services off to any other service provider that you want.  I chose to use deSEC.io, which is in Germany and is also free.  I also see it popping up as an option in places where you need to pick your DNS service for automated processes.  So it’s a good choice for me.</p>

<p>In my GoDaddy account, I just go to the area for my domain, pick the “DNS” section and plug in the servers for deSEC, which are <code class="language-plaintext highlighter-rouge">ns1.desec.io</code>, and <code class="language-plaintext highlighter-rouge">ns2.desec.org</code>.  That’s it.  From then on I just use my account at deSec to manage my DNS.</p>

<h1 id="conclusion">Conclusion</h1>

<p>Having your own public domain with a public DNS server is a great idea because it removes a lot of friction from a few key infrastructure tasks that you’re going to need to do to run a homelab.</p>]]></content><author><name>Dave Barrett</name><email>pragmaticcoding8@gmail.com</email></author><category term="homelab" /><summary type="html"><![CDATA[If you are self-hosting, then you should probably have a public domain registered. In this article, we'll look at why you should do this, and the things that it helps.]]></summary></entry><entry><title type="html">Managing DNS Services</title><link href="https://www.pragmaticcoding.ca/homelab/dns-concepts" rel="alternate" type="text/html" title="Managing DNS Services" /><published>2026-06-23T17:00:00+00:00</published><updated>2026-06-23T17:00:00+00:00</updated><id>https://www.pragmaticcoding.ca/homelab/dns-concepts</id><content type="html" xml:base="https://www.pragmaticcoding.ca/homelab/dns-concepts"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>Domain Name Servers (DNS Servers) are one of the cornerstones of both the Internet, and your own self-hosted infrastructure.  In this article we are going to look at what DNS servers do, how they work and integrate with each other, and how to host your own private DNS server for your homelab.</p>

<h1 id="the-absolute-basics---addresses-and-names">The Absolute Basics - Addresses and Names</h1>

<p>When you strip away everything else, every device connected to a TCP/IP network - which is just about everything we deal with here - is uniquely identified with an IP address.  There are two versions of IP addresses in use now: IPv4 and IPv6.  We aren’t going to talk about IPv6 in this article because it’s a subject all by itself.  So, from now on when I refer to IP address, I’m talking about IPv4 addresses.</p>

<p>IPv4 addresses are 32 bits long and divided into 4 octets of 8 bits.  So, 4 bytes.  They look like 192.168.1.1.  Each byte, obviously, can have a value between 0 and 255.</p>

<p>The problem with IP addresses is that are clumsy and hard to remember.  They are also absolutely attached to a particular machine - this becomes an important limitation later on.</p>

<p>Finally they don’t have a hierachical structure that maps nicely to the real world relationships behind how the Internet is organized.</p>

<p>To overcome these issues, we use names to refer to devices on the Internet instead, and everybody is walking around with a whole bunch of these device names in their heads.  Furthermore, the names are structured in a hierachical manner that lines up nicely with the way that the Interet is actually organized.  This structure also makes them easier to remember.</p>

<p>Internet names are divided up into sections separated by <code class="language-plaintext highlighter-rouge">.</code> characters.  They go from most specific on the left, to least specific on the right.  Let’s look at a typical sort of name: <code class="language-plaintext highlighter-rouge">server.dmz.organization.com</code>, and see how it is organized.</p>

<p>At the far left is the specific machine, <code class="language-plaintext highlighter-rouge">server</code>.  It is located inside the domain <code class="language-plaintext highlighter-rouge">.dmz.organization.com</code>.  Next along is <code class="language-plaintext highlighter-rouge">.dmz</code> which is a domain, but it is a subdomain of <code class="language-plaintext highlighter-rouge">.organization.com</code>.</p>

<p>If we keep going to the right, we get to <code class="language-plaintext highlighter-rouge">.organization</code>, which is a subdomain of <code class="language-plaintext highlighter-rouge">.com</code>.  And, of course, we have <code class="language-plaintext highlighter-rouge">.com</code> which is what is referred to as a “top level domain”.  You can think of this as three domains: <code class="language-plaintext highlighter-rouge">.dmz.organization.com</code>, <code class="language-plaintext highlighter-rouge">.organization.com</code> and <code class="language-plaintext highlighter-rouge">.com</code>.  Of course, both <code class="language-plaintext highlighter-rouge">.dmz.organization.com</code>, and <code class="language-plaintext highlighter-rouge">.organization.com</code> are both subdomains of the domains to the right.</p>

<p>Each domain has absolute control over its own contents.  This means that whatever body owns/controls the <code class="language-plaintext highlighter-rouge">.com</code> domain is the only body allowed to add or remove anything to/from it.  In order for <code class="language-plaintext highlighter-rouge">.organization.com</code> to exist, the owner of <code class="language-plaintext highlighter-rouge">.com</code> had to add it.</p>

<p>In this example, the subdomain <code class="language-plaintext highlighter-rouge">.dmz.organization.com</code> looks like it’s simply an administrative division inside of the <code class="language-plaintext highlighter-rouge">.organization.com</code> domain.  It’s highly likely that the owner of <code class="language-plaintext highlighter-rouge">.organization.com</code> created it for the use of <code class="language-plaintext highlighter-rouge">.organization.com</code>, and not as a separately administered subdomain.  This is quite normal, and any owner of a domain can freely add whatever subdomains to their domain that they want, for whatever purposes.</p>

<h1 id="domain-name-servers">Domain Name Servers</h1>

<p>What we need in order to route traffic to <code class="language-plaintext highlighter-rouge">server.dmz.organization.com</code> is a way to turn that name into an IP address.  This is where name servers, or DNS Servers (the “DNS” refers to “Domain Name System”), come into play.</p>

<p>In a nutshell, the domain that “owns” a name is responsible for providing the translation for that name into an IP address.  In fact, a name on the Internet (or any IP network for that matter) simply doesn’t exist unless it’s discoverable in its domain’s DNS server.</p>

<p>This means that for every domain on any IP network, there has to be a DNS server that can give the IP address for every name in that domain.</p>

<p>Now, if you type <code class="language-plaintext highlighter-rouge">http://server.dmz.organization.com</code> into your browser then how does your browser know where the DNS server for <code class="language-plaintext highlighter-rouge">.dmz.organization.com</code> is?  How does it query it?</p>

<p>The answer is very simple.  Each parent domain has to have a an entry in its DNS server that tells everyone where the DNS servers for every single subdomain it holds is.</p>

<p>This means that your browser has to go and query the <code class="language-plaintext highlighter-rouge">.com</code> domain to find out the location of the DNS server for <code class="language-plaintext highlighter-rouge">.organization.com</code>.  Then it has to query <em>that</em> DNS server for the address for <code class="language-plaintext highlighter-rouge">server.dmz.organization.com</code>.  It might be that <code class="language-plaintext highlighter-rouge">.dmz.organization.com</code> has its own DNS server, in which case the query have to continue at that server.</p>

<p>At least that’s the naive view of how it works.  In reality, your browser makes a single request to its own local name server and the rest of the work is done by that.  Your browser isn’t aware of how the name is resolved, just that it happens.</p>

<p>When the DNS servers talk to each other, the process iterates over and over.  Your local DNS server will answer the request if it has the information, otherwise it passes the request on to <em>its</em> upstream DNS server, which then goes through the same process.  Eventually, some name server will query <code class="language-plaintext highlighter-rouge">.com</code> for the DNS server of <code class="language-plaintext highlighter-rouge">organization.com</code> and it will give the answer.  At that point the process rolls itself back up, each name server passing back the answer until it eventually gets back to your browser.</p>

<p>[Caveat: This is a good way to conceptialize how DNS servers communicate, meaning that it provides a working model that explains the behaviours that you see and the things that you need to do in order to properly configure a DNS server.  Under the hood, this may not match up with how modern DNS servers actually go about doing what they do.]</p>

<p>There was a critical component in this description: “DNS server will answer the request if it has the information”.  How can it have the answer if it isn’t the DNS server for the domain specified?</p>

<p>The truth is that going through this entire process for every single request would be an incredibly wasteful approach.  In reality, DNS servers cache the results that they’ve looked up for a period of time.  DNS entries, for the most part, don’t change very often so it’s safe to store them for a while.  In fact, every DNS entry includes a “Time to Live” (TTL) value.  This is the number of seconds for which the answer can be assumed to be valid.  DNS caches will respond immediately with values that haven’t yet reached their TTL.</p>

<p>This means that a DNS server can return two types of values:</p>

<dl>
  <dt>Authoritative</dt>
  <dd>
    <p>This is when the DNS server that responded to the request is the designated DNS server for the domain that was queried.</p>
  </dd>
  <dt>Non-Authoritiative</dt>
  <dd>
    <p>This is when the DNS server that responded to the request is NOT the desginated DNS server for the domain that was queried.</p>
  </dd>
</dl>

<p>I’m only defining this because we’ll need to understand this a little when we talk about setting up a DNS server.  In real life, you rarely care whether or not an answer was authoritative.  In those cases that you do, it’s because something went wrong.</p>

<h2 id="two-uses-for-dns-servers">Two Uses for DNS Servers</h2>

<p>From the section above you can see that DNS servers have two direct uses.</p>

<ol>
  <li>To act as an agent to look up names from around the Internet.</li>
  <li>To act as the authoritative definition of the contents of a domain.</li>
</ol>

<p>When you are setting up a homelab, you’re going to want your DNS server to act as both.</p>

<h1 id="what-information-is-in-a-dns-server">What Information is in a DNS Server?</h1>

<p>The contents of a DNS server are organized into “zones”.  Just about every DNS server you’ll encounter will contain multiple zones.  Back in the old days, before we had GUI’s to manage the DNS servers, we used to configure them by creating “Zone Files” which were just text files in a particular format that the DNS server could parse.  Dinosaurs like me still refer to the contents of a zone as a “Zone File” even though they aren’t really used any more.  Saying, “Put it in the zone file”, is just a susinct way of refering to the process of configuring a zone in a DNS server.</p>

<p>All records in a zone file have at least these three elements:</p>

<ol>
  <li>Type</li>
  <li>Name</li>
  <li>Value</li>
</ol>

<p>Generally, DNS lookups are done by specifying the type and and the name.  It is possible, even common, to do searches for just a name, and accept any type of records that are returned.  It’s also possible to do searches for just type, for instance if you are looking for MX records to find the email servers for a domain.</p>

<p>What kind of entries might you find in a zone file?</p>

<dl>
  <dt>SOA - Start of Authority</dt>
  <dd>
    <p>This entry specifies the <em>authoritive</em> information about the zone.  The name of the record is the name of the zone, but you’ll usually see it listed as <code class="language-plaintext highlighter-rouge">@</code>.  The value is structured, and amongst other things will tell you the name of the primary nameserver for the domain.</p>
  </dd>
  <dt>A - Address</dt>
  <dd>
    <p>This is your bread and butter entry.  The entry has a name, and the value is an IP address.</p>
  </dd>
  <dt>CNAME - Redirection</dt>
  <dd>
    <p>The most common use you’ll find for these is when you want to use multiple names for the same address, but you don’t want to have to update multiple entries if that address changes.  A CNAME entry has a name of its own, and then returns the name of another entry that it shares an address with.  An important point here is that if a DNS lookup encounters a CNAME, then it will continue on but now doing a lookup for the value in the CNAME record.</p>
  </dd>
  <dt>TXT - Text</dt>
  <dd>
    <p>TXT entries are generic informational entries that don’t have any defined purpose in the DNS system.  Lot’s of proprietary systems use them to store publicly visible information that they need for their processes.</p>
  </dd>
  <dt>MX - Mail Exchange</dt>
  <dd>
    <p>These entries tell the internet where the email servers for your domain are located.  These can be either IP addresses or names that can be looked up via DNS searches.</p>
  </dd>
  <dt>NS - Name Server</dt>
  <dd>
    <p>Like <code class="language-plaintext highlighter-rouge">SOA</code> these have a name of <code class="language-plaintext highlighter-rouge">@</code>, and they contain the names of authoritative DNS servers for the domain.  There can (should) be more than one for each domain.</p>
  </dd>
  <dt>PTR - Pointer</dt>
  <dd>
    <p><code class="language-plaintext highlighter-rouge">PTR</code> records are a bit like <code class="language-plaintext highlighter-rouge">CNAME</code> records, but they won’t cause DNS lookups to carry on looking up the new value.  You are going to see these used in “reverse lookups” more often than not.  A reverse lookup uses DNS to locate the name of an entry from its IP address.  Most DNS server software will facilitate the creation of reverse lookup zones for this purpose.</p>
  </dd>
</dl>

<p>You may encounter other types (there are lots more), but for a homelab these are the ones you’re most likely to care about.</p>

<h1 id="managing-your-own-dns-server">Managing Your Own DNS Server</h1>

<p>Unless you want to remember a bunch of IP addresses - which means you’re never going to be able to expose your services to the non-technical people in your family - you’re going to need your own name server.  At a minimum, you need to be able to assign names to the servers in your network.</p>

<p>You have choices.  If you are running on OPNSense firewall, you can use one of the services it hosts to run your DNS.  Or you can spin up a container in Proxmox and run one of the popular DNS servers.</p>

<p>Lots of people seem to come into DNS through PiHole or AdGuard without even realizing that are running a DNS server.</p>

<p>Personally, I prefer Technitium’s DNS server.  It’s easy to install and configure, supports all the ad blocking you could ever want and has a bunch of advanced features that you’ll probably want to use sooner or later.  The GUI interface is awesome and intuitive and the whole thing <em>just works</em> without any hitches.</p>

<p>That’s what I’m going to talk about here.</p>

<h2 id="installing-technitium">Installing Technitium</h2>

<p>I installed Technitium from the Promox Community Scripts, and I think that’s the probably the easiest way to get it up and running.</p>

<p>The Script page is <a href="https://community-scripts.org/scripts/technitiumdns">here</a>.  If you go there, you’ll see that the script creates an LXC guest, and defaults to using a single CPU core with 512MB of RAM.  It’s fairly light-weight, as you would expect.</p>

<p>In the “INSTALL” section, there’s a <code class="language-plaintext highlighter-rouge">bash</code> command that will download the script and run it for you.  You copy that command (there’s a little copy icon right at the end of the command), and then paste it into the shell of your Promox node.  It will then walk you through a series of questions that will determine how the LXC is configured.</p>

<p>The first question asked by the scripts is whether you want to just use the “Default” or “Advanced” installation (at least those are the first two options).  I always pick the “Advanced” because there are a couple of networking options that I want to customize, and I don’t remember seeing them the very first time I ever installed anything using “Default”.  I also pick the “Verbose” output so that I can at least see that progress is still happening as the install runs, and I’m not sitting there staring at an unchanging screen wondering if it has got stuck or not.  Other than that, I usually just accept all of the default values as they come up.</p>

<p>When it’s done, it will tell you the address and port to log into the DNS Server’s administrative GUI.  Plug that into your browser and, after you’ve logged in, you’ll see a screen that looks like this:</p>

<p><img src="/assets/homelab/Technitium.png" alt="ScreenShot 1" /></p>

<p>The “Menu” is actually just the tabs across the top of the page.</p>

<h2 id="upstream-lookups">Upstream Lookups</h2>

<p>In order to use your DNS server, you’ll need to make it the primary DNS server for your network.  Obviously, systems on your network will need to query this DNS server if they are going to be able to lookup names on your network.</p>

<p>But once you do this, then all of the searches from your network are going to go through this DNS server.  Even the ones for names in other domains out there in the Internet.  So before you can do this, you’ll need to tell Technitium how you want it forward your requests for lookups outside your domain.</p>

<p>To set this up, go to Settings –&gt; Proxy &amp; Forwarders and scroll down to the second section.  You’ll see something like this:</p>

<p><img src="/assets/homelab/dns_forwarding.png" alt="Forwarders" /></p>

<p>Technitium has a reasonable selection of commonly used public DNS servers that you can quickly select.  You can, of course, put in the addresses or names of any other DNS servers that you would like to use, including the ones from your ISP - if you really want to do that.  You should pick at least 2 from at least two different providers.</p>

<p>The “Save” button is at the bottom of the page.  Now you can move on to creating a zone.</p>

<h2 id="creating-a-zone">Creating a Zone</h2>

<p>Before we get into this, I strongly recommend that you keep your domain flat with a single zone.  Unless you’re running some kind of complex organization with mulitple sub-units that need to independently assign names without worrying about collisions with names in other sub-units, you’ll just be making your life more complicated than you need to if you start adding subdomains and zones.</p>

<p>So stick to one zone, and come up with a naming scheme that avoids collisions in that one zone.</p>

<p>From the main Dashboard screen pick the “Zones” tab, and then at the top left you’ll see a button called “Add Zone”.  Click it, and you’ll get a screen that looks like this:</p>

<p><img src="/assets/homelab/Add_Zone.png" alt="Add Zone" /></p>

<p>For internal purposes, you don’t <em>have</em> to have a properly registered domain to set up a DNS server.  You can call this zone anything you want, but be aware that if you replicate some properly registered domain name then you won’t be able to access that external domain from inside your network.  That being said, having an actual registered domain with an public, external DNS server is a good idea.</p>

<p>For now, just pick “Primary Zone”.  If you do have a registered domain that you also want to use for internal purposes (I would recommend this), then you’ll eventually want to change it to “Conditional Forwarder Zone” (see the section below).</p>

<p>That’s all you need to do.  The “Catalog Zone” area is for configuring a failover if you have more than 1 Technitium DNS server in your network.  It’s a good idea to do this, but beyond the scope of this discussion.</p>

<p>One last point.  If you click on the link: “Help: How To Self Host Your Own Domain Name” it will take you to an article about how to host a <em>public</em> DNS server for your registered domain.  That is not what we are doing here, so that article is not relevant.</p>

<h2 id="adding-entries">Adding Entries</h2>

<p>Once you’ve clicked “Add” on the previous screen, you’ll end up with a page that looks like this:</p>

<p><img src="/assets/homelab/NewZone.png" alt="New Zone" /></p>

<p>For free, you get an <code class="language-plaintext highlighter-rouge">SOA</code> record and an <code class="language-plaintext highlighter-rouge">NS</code> record that points to your Technitium server.  For internal purposes, neither of these two records is particularly important.  The default values in the <code class="language-plaintext highlighter-rouge">SOA</code> will work just fine, and you’ll probably be publishing this server via its IP address with either DHCP, or the static info you set up in all of your devices.  So the <code class="language-plaintext highlighter-rouge">NS</code> record will never be used in real life.</p>

<p>Now you can add your first address.  Click on “Add Record”:</p>

<p><img src="/assets/homelab/Add_A.png" alt="Add Address" /></p>

<p>Make sure that “Type” says “A”.  Fill in the name that you want to add…</p>

<p>Ok, naming stuff is hard.  It’s probably the single hardest aspect of programming, and it’s only slightly less hard with network administration.  Things to keep in mind:</p>

<dl>
  <dt>Names need to be memorable</dt>
  <dd>
    <p>This is the point of using names.  Trust me, you don’t want to be surfing over to your DNS server to see what you called something every time you want to open it in a browser.</p>
  </dd>
  <dt>Names need to be specific</dt>
  <dd>
    <p>It’s probably not obvious now, but you are going to find that you have <strong>servers</strong> and you have <strong>services</strong>.  While each service is hosted by a server, they are not the same thing.  And sometimes you’ll have several services hosted by the same server.  When you start out naming things in your network, you are probably going to be defining servers, not services.  So name your servers as servers.</p>
  </dd>
  <dt>Names need to follow a scheme</dt>
  <dd>
    <p>This because you will forget the names.  But, if you follow a scheme, then you can probably figure out the name you want by re-applying the scheme.  A bit of a pain, but still easier than looking it up in the Technitium GUI.</p>
  </dd>
  <dt>Names need to be a short as possible</dt>
  <dd>
    <p>Address bars are small, and typing stuff into a phone browser is always painful.</p>
  </dd>
  <dt>Names need to be consistent</dt>
  <dd>
    <p>Compose your names the same way each time.  If you have 3 Proxmox backup servers then name them <code class="language-plaintext highlighter-rouge">pb1</code>, <code class="language-plaintext highlighter-rouge">pb2</code> and <code class="language-plaintext highlighter-rouge">pb3</code>, or <code class="language-plaintext highlighter-rouge">pb0</code>, <code class="language-plaintext highlighter-rouge">pb1</code> and <code class="language-plaintext highlighter-rouge">pb2</code> (if you are a programmer).  Or name them <code class="language-plaintext highlighter-rouge">firstpb</code>, <code class="language-plaintext highlighter-rouge">secondpb</code> and <code class="language-plaintext highlighter-rouge">thirdpb</code> if you hate yourself.  Then if you decide to add more name servers, <strong>use the exact same convention</strong>.  Don’t call them <code class="language-plaintext highlighter-rouge">ns_1</code>, <code class="language-plaintext highlighter-rouge">ns_2</code> and <code class="language-plaintext highlighter-rouge">ns_3</code> if you used <code class="language-plaintext highlighter-rouge">pb1</code>, <code class="language-plaintext highlighter-rouge">pb2</code> and <code class="language-plaintext highlighter-rouge">pb3</code>.  And don’t call them <code class="language-plaintext highlighter-rouge">ns1</code>, <code class="language-plaintext highlighter-rouge">ns2</code> and <code class="language-plaintext highlighter-rouge">ns3</code> if you used <code class="language-plaintext highlighter-rouge">pb0</code>, <code class="language-plaintext highlighter-rouge">pb1</code> and <code class="language-plaintext highlighter-rouge">pb2</code>.  You get the idea.</p>
  </dd>
  <dt>Names can be hard to change</dt>
  <dd>
    <p>Just try and change a name after you’ve used it on 4 people’s phone apps, referenced it for an API call on 3 different servers, and imbedded it into your smart TV.  Eventually you’ll have to do this, and then you’ll wish you listened to me.  Try to get your naming scheme figured out at the beginning, so you’re not changing names over and over.</p>
  </dd>
</dl>

<p>You’ll have to trust me.  These things are important.  Also, remember that you can use <code class="language-plaintext highlighter-rouge">CNAME</code> entries if you want shortcut names for particular servers or services.  You can also use them to help transition through changes to your naming scheme.</p>

<p>Moving on with this screen:  I wouldn’t mess with the TTL unless you are planning to change the address as part of a setup process or something.  In that case, set it really low, like 30, so that your workstation won’t cache the lookups and cause grief.  Once you do settle in on a final address, change the TTL back to 3600.</p>

<p>The “Add reverse (PTR) record”, and the “Create reverse zone for PTR record”, are potentially useful.  <strong>IFF</strong> the address is unique to the name, then just click these two boxes on.  This will cause Technitium to create a reverse lookup entry (and potentially the zone to put it in) which will let you find the name from the IP address.</p>

<p>When you need this, you need this.  And it costs almost nothing.</p>

<p>If you’ve got 5 names that all point to the same address, then you’ll only want to check off these two boxes when you create what you would consider to be the “primary” name for that address.</p>

<p>I find it useful to put something in comments.  At least explain what the name is in plain English.  Is it a server or a service?  Also, if it’s a service and it has a particular port that you need to use to access it, I’d put it in the comments.  It saves having to look through you browser history, or checking the GitHub pages for the project to find how to access the service.</p>

<p>That’s click “Save”.  Rinse and repeat for all of the other names you want to create in your network.</p>

<h2 id="conditional-forwarding">Conditional Forwarding</h2>

<p>This is a bit advanced in concept, but easy in practice.</p>

<p>There are lots of good reasons for registering your domain name, and using a publicly accessible DNS server for that domain.  For instance, you’ve set up your email so that you can have your own email addresses in that domain.  Something like <code class="language-plaintext highlighter-rouge">fred@myfamily.org</code>.  In order to be able to receive email for that domain, other mail servers need to be able to find the MX records for <code class="language-plaintext highlighter-rouge">myfamily.org</code>.  And that means you’ll be using some DNS service for you domain out there in the Internet somewhere, something like CloudFlare.</p>

<p>But now you’ve just set up an authoritative DNS server for <code class="language-plaintext highlighter-rouge">myfamily.org</code> inside your home network.  Nobody outside your network can see it or use it, which is good.  But anybody inside your network will never see entries that are in that CloudFlare DNS server.  And that can be a problem.</p>

<p>What you want is for your local, private DNS server to answer any lookup for <code class="language-plaintext highlighter-rouge">myfamily.org</code> from its own records if it can, and if that lookup fails, then to forward the request out to the Internet and let the CloudFlare DNS server answer it.</p>

<p>This is called “Conditional Forwarding”, and it’s pretty powerful and easy to set up.  All you have to do is to change the type of your zone from “Primary” to “Conditional Forwarding”.</p>

<p>Go into your zone, and at the top of the screen will be a button called “Options” with triangle, click it and you’ll get a drop-down meun.  Select “Convert Zone” and you’ll get a screen like this:</p>

<p><img src="/assets/homelab/Conditional.png" alt="Convert to Conditional" />]</p>

<p>Make sure that “Conditional Forwarder Zone” is selected and click on “Convert Zone”.  All done.</p>

<h2 id="ad-blocking">Ad Blocking</h2>

<p>If you’re not coming from the PiHole/Adguard world, you may wonder how a DNS server can block ads.</p>

<p>Nobody embeds actual IP addresses in anything, not even for ads.  If you look at the source for any web page that has ads, you’ll see URL’s for the ad content buried in the source.  Things like <code class="language-plaintext highlighter-rouge">https:\\clickme.spammersrus.org\adcontent8873923</code>.  You browser has to perform a lookup to find the address for <code class="language-plaintext highlighter-rouge">clickme.spammersrus.org</code>, in order to then connect to that server and download the ad content.</p>

<p>But what if your DNS server refused to return a result for <code class="language-plaintext highlighter-rouge">clickme.spammersrus.org</code>?</p>

<p>That’s exactly how DNS ad blocking works.  The DNS server has a list of servers or domains that it knows are used for ad content, and it simply won’t resolve them.  For anyone.  This means that the ad content is completely blocked from you network.  Not just browsers, but apps on any phone on your WiFi.  No more ads at the bottom of your Sudoku app.  No more ads baked into any servers that you host.  None.</p>

<p>And since everybody hates ads, there are lots of people and organizations out there who compile and update lists of all of the sites known to serve up ad content.  And you can tell Technitium which lists you want to use.</p>

<p>Setting up ad blocking in Technitium is really easy.  In the “Settings” tab there is a sub-tab called “Blocking”.  It’s all there.</p>

<p>The first check boxes turn it on.</p>

<p>Then next allows you to disable it temporarily, and allow you to specify some address that you’ll exempt from the blocking.  Maybe you’ve gotten one of those “Please disable your ad blocker” pop-ups that blocks the content and you really want to see that content?  Not for me, but this is how you would temporarily turn off the ad blocking.</p>

<p>“Blocking Type”, tells Technitium how to respond when it blocks an address.  I just stick with the <code class="language-plaintext highlighter-rouge">NXDOMAIN</code> response, which seems to work well.</p>

<p>Then we get to the the lists and list management.  There are quite a few lists pre-loaded into Technitium, but if you want to you can links to whatever lists you can find.  You’ll also notice that these won’t just block ads, but will also block content of various types.  For instance, you can get lists with gambling sites, adult content, or malicious content.</p>

<p>The best recommendations are to start off with a limited amount of blocking and see how it works.  Some lists are much more aggressive than others, and will include sites that might block content that you actually want to let through.  If you are finding that this is happening, then shift to a less aggressive list.  If too much is still getting through, then add more lists or switch to more aggressive lists.</p>

<p>One other thing:  You can also use an upstream public DNS that also does some of this filtering and blocking as well.  I’ve used some of these with my Android phone, and they do work quite well.  But Technitium does a good job in this regard, so I don’t use them for my network.</p>

<h2 id="encrypted-lookups">Encrypted Lookups</h2>

<p>Obviously, all of your traffic goes out through your ISP to the Internet, and this includes your DNS lookups.  It’s highly likely that your ISP is analyzing your DNS lookups in order to monetize them through ad revenue.  At least this is the accepted belief.</p>

<p>Technitium makes it trivial to encrypt your DNS queries via DNS-over-TLS, so just do it.  The one caveat is that not all upstream DNS servers support DNS-over-TLS.  However, CloudFlare, Google and Quad9 all support it, so this shouldn’t be an issue.</p>

<p>Also, remember that there’s no point encrypting DNS queries to your ISP’s DNS servers since they can always see what’s going on at the DNS server end.</p>

<p>To turn it on, go back to this screen:</p>

<p><img src="/assets/homelab/dns_forwarding.png" alt="Forwarders" /></p>

<p>And click on “DNS-over-TLS”.  Make sure that the forwarders that you have selected support this protocol.</p>

<h1 id="conclusion">Conclusion</h1>

<p>Although DNS servers are the backbone of so much of the Internet, and they can do some really, really complicated stuff, setting up your own DNS server for you homelab is fairly easy.</p>

<p>Realistically speaking, this should probably be the first server that you set up on your homelab.  For one, it’s very straight-forward and just works.  This makes it a good learning experience and will introduce you to a lot of homelab concepts.  On top of that, you’re going to want to start using names right away, so you’ll need a DNS server to do that.</p>]]></content><author><name>Dave Barrett</name><email>pragmaticcoding8@gmail.com</email></author><category term="homelab" /><summary type="html"><![CDATA[Everything you need to know about DNS if you are self-hosting. How to use Technitium to host a private DNS server in your homelab.]]></summary></entry><entry><title type="html">Whole Home Audio With SnapCast - Part III</title><link href="https://www.pragmaticcoding.ca/homelab/snapcast2" rel="alternate" type="text/html" title="Whole Home Audio With SnapCast - Part III" /><published>2026-06-22T17:00:00+00:00</published><updated>2026-06-22T17:00:00+00:00</updated><id>https://www.pragmaticcoding.ca/homelab/snapcast2</id><content type="html" xml:base="https://www.pragmaticcoding.ca/homelab/snapcast2"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>This is the third part of an article describing how to set up whole-home audio using Mopidy and SnapCast.  If you haven’t already read them should should probably read <a href="/homelab/snapcast0">Part I</a> and <a href="/homelab/snapcast1">Part II</a> first.</p>

<p>So far, we’ve installed and configured the SnapCast server, which will broadcast the music to the SnapCast clients.  We’ve looked at how to set up a RasberryPi Zero (RPi0), install a Digital to Analog Converter (DAC) on it, install the SnapCast client service, and then configure the whole thing so that it can receive a stream from the SnapCast server and play it through the DAC.</p>

<p>What we need now is a way to pull an audio stream from the Internet, and transfer it over to our SnapCast server to broadcast internally.  For this we’ll use an application called Mopidy.</p>

<p>Mopidy is a streaming client designed to be used on a “headless” server.  It has a variety of plugins to provide a browser interface and to allow it to connect to diffent kinds of streaming services.  It also has a REST API that we will be using to provide some simple automation for the whole-home audio.</p>

<p>In my house we are mostly streaming from SomaFM.com, which is an excellent service and has a variety of stations to choose from.  Personally, we like the one called “Groove Salad”.  That’s what I’ll be showing how to set up.  However, if you are interested in streaming from any other service, this guide should get you going on that too.</p>

<h2 id="mopidy-server">Mopidy Server</h2>

<p>Mopidy is pretty easy to install, but there are a few pain points along the way…</p>

<p>Start by using <code class="language-plaintext highlighter-rouge">apt</code> to install Mopidy.  This seems to give the latest version:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">sudo </span>apt update
<span class="nv">$ </span><span class="nb">sudo </span>apt <span class="nb">install </span>mopidy
</code></pre></div></div>
<p>You can check the version that you get:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>mopidy <span class="nt">--version</span>
Mopidy 3.4.2
</code></pre></div></div>
<p>Which, at the time of writing, was the latest version, although from October 2023.</p>

<p>You can use <code class="language-plaintext highlighter-rouge">apt</code> to see all of the Mopidy extensions that are available that way:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">sudo </span>apt search mopidy
</code></pre></div></div>
<p>We are going to need a couple of extensions, otherwise we won’t be able to do much with Mopidy.  The first we will install is the extension for SomaFM.  If you are going to be using some other service for a source, you’ll need to install the extension for it.  However, I would still recommend that you install the SomaFM add-on because then you’ll have something that you <em>can</em> test before you go trying your own thing.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">sudo </span>apt <span class="nb">install </span>mopidy-somafm
</code></pre></div></div>
<p>Next, we’ll need a web interface so that we can control everything.  I prefer “Muse” because it integrates some of the controls for SnapCast as well.</p>

<p>Many of the extensions, like Muse, cannot be installed via <code class="language-plaintext highlighter-rouge">apt</code>.  You’ll need to use a Python utility to do it, and the first thing you’ll need to do is install that utility.  Here’s how you get Muse installed:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">sudo </span>apt <span class="nb">install </span>python3-pip
<span class="nv">$ </span><span class="nb">sudo </span>python3 <span class="nt">-m</span> pip <span class="nb">install </span>Mopidy-Muse
<span class="nv">$ </span><span class="nb">sudo </span>python3 <span class="nt">-m</span> pip <span class="nb">install</span> <span class="nt">--break-system-packages</span> Mopidy-Muse
<span class="nv">$ </span><span class="nb">sudo </span>python3 <span class="nt">-m</span> pip <span class="nb">install</span> <span class="nt">--break-system-packages</span> Mopidy-Jellyfin
</code></pre></div></div>
<p>You can see that I’ve also installed the Jellyfin add-on as well.</p>

<p>That <code class="language-plaintext highlighter-rouge">--break-system-packages</code> option is going to generate some ugly warning messages.  If you leave it out, you’ll get this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">sudo </span>python3 <span class="nt">-m</span> pip <span class="nb">install </span>Mopidy-Muse
error: externally-managed-environment

× This environment is externally managed
╰─&gt; To <span class="nb">install </span>Python packages system-wide, try apt <span class="nb">install
    </span>python3-xyz, where xyz is the package you are trying to
    install.

    If you wish to <span class="nb">install </span>a non-Debian-packaged Python package,
    create a virtual environment using python3 <span class="nt">-m</span> venv path/to/venv.
    Then use path/to/venv/bin/python and path/to/venv/bin/pip. Make
    sure you have python3-full installed.

    If you wish to <span class="nb">install </span>a non-Debian packaged Python application,
    it may be easiest to use pipx <span class="nb">install </span>xyz, which will manage a
    virtual environment <span class="k">for </span>you. Make sure you have pipx installed.

    See /usr/share/doc/python3.13/README.venv <span class="k">for </span>more information.

note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing <span class="nt">--break-system-packages</span><span class="nb">.</span>
hint: See PEP 668 <span class="k">for </span>the detailed specification.
</code></pre></div></div>
<p>I’m not a Python programmer, and I have absolutely no knowledge of or experience with “Python virtual environments”.  When I first encountered this, I tried to figure it out.  I really tried.  In the end, I gave up and just went with the <code class="language-plaintext highlighter-rouge">--break-system-packages</code>, even though it made me feel uneasy.  It hasn’t caused any problems, though.</p>

<p>The last bit of installation that you’ll need to do is to install some decoders for the streaming engine.  Mopidy uses something called
<code class="language-plaintext highlighter-rouge">GStreamer1.0</code> to do the work.  You’ll already have <code class="language-plaintext highlighter-rouge">GStreamer1.0</code> installed, but it will need some additional packages to make it work. There’s three of them; “Good”, “Bad”, and “Ugly”.  I don’t know which ones you absolutely need, so I installed all of them:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">sudo </span>apt <span class="nb">install </span>gstreamer1.0-plugins-good
<span class="nv">$ </span><span class="nb">sudo </span>apt <span class="nb">install </span>gstreamer1.0-plugins-bad
<span class="nv">$ </span><span class="nb">sudo </span>apt <span class="nb">install </span>gstreamer1.0-plugins-ugly
</code></pre></div></div>
<p>That’s it for the installation.  The last thing you need is the configuration file to make everything work.  It’s located at <code class="language-plaintext highlighter-rouge">/etc/mopidy/mopidy.conf</code>, and when you are done it should look something like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># For information about configuration values that can be set in this file see:</span>
<span class="c">#</span>
<span class="c">#   https://docs.mopidy.com/en/latest/config/</span>
<span class="c">#</span>
<span class="c"># Run `sudo mopidyctl config` to see the current effective config, based on</span>
<span class="c"># both defaults and this configuration file.</span>
<span class="o">[</span>core]
restore_state <span class="o">=</span> <span class="nb">true</span>

<span class="o">[</span>http]
<span class="nb">hostname</span><span class="o">=</span>10.93.50.208

<span class="o">[</span>audio]
output <span class="o">=</span> audioresample <span class="o">!</span> audioconvert <span class="o">!</span> audio/x-raw,rate<span class="o">=</span>48000,channels<span class="o">=</span>2,format<span class="o">=</span>S16LE <span class="o">!</span> wavenc <span class="o">!</span> filesink <span class="nv">location</span><span class="o">=</span>/var/local/snapserver/snapfifo
mixer_volume <span class="o">=</span> 22
buffer_time <span class="o">=</span> 4000

<span class="o">[</span>somafm]
encoding <span class="o">=</span> aac
quality <span class="o">=</span> highest

<span class="o">[</span>muse]
enabled <span class="o">=</span> <span class="nb">true
</span>mopidy_host <span class="o">=</span> 10.93.50.208
mopidy_port <span class="o">=</span> 6680
mopidy_ssl <span class="o">=</span> <span class="nb">false
</span>snapcast_host <span class="o">=</span> 10.93.50.208
snapcast_port <span class="o">=</span> 1780
snapcast_ssl <span class="o">=</span> <span class="nb">false</span>

<span class="o">[</span>jellyfin]
<span class="nb">hostname</span> <span class="o">=</span> 192.168.1.211
username <span class="o">=</span> mopidy
password <span class="o">=</span> PYo78u.p%.MChCTMROE83M&amp;#867F
max_bitrate <span class="o">=</span> 100
</code></pre></div></div>
<p>Obviously, you’ll have to change the IP addresses to match you’re own setup.  The <code class="language-plaintext highlighter-rouge">restore_state</code> flag just tells Mopidy to resume doing whatever it was doing before it was restarted.</p>

<p>The <code class="language-plaintext highlighter-rouge">output</code> tag is every important.  Make sure that the “filesink location” matches the fifo pipe that you created for SnapCast.  If you followed my instructions, it should be <code class="language-plaintext highlighter-rouge">/var/local/snapserver/snapfifo</code>.</p>

<p>If you added any other extensions, you’ll probably have to add a section in this file for that extension.  Generally, if you follow a link from the <a href="https://mopidy.com/ext/">Mopidy extension listing</a>, you’ll get to a page that has a GitHub link listed just above the section called “Installation”.  Follow that link and the GitHub page should have a sample configuration block on it.</p>

<p>At this point, you can restart the Mopidy service to get it to load all of the extensions and read the configuration.</p>

<p>You should use <code class="language-plaintext highlighter-rouge">systemctl status mopidy -n60</code> to make sure that it’s working properly.  If you’ve muddled up the configuration, you should get some messages in that display that will tell you that it had problems.</p>

<h3 id="testing-it-all">Testing It All</h3>

<p>I’d suggest opening up two browser tabs: one with the SnapCast page at port 1780, and the other at the Mopidy page at port 6680, then click into the “Muse” page.</p>

<p>On the SnapCast tab, click on the “Play” button.  Then go to the Muse page, pick “Browse”, then select “SomaFM” which will expand the station list.  Click on one of the stations, then “Play”.  If all goes well, you should hear music through your browser.</p>

<h1 id="automation">Automation</h1>

<p>In our house, we want the whole home audio streaming SomaFM’s “Groove Salad” from the time we wake up until we go to bed.  When our current dog was just a puppy and we had her spending the night in a crate beside the bed, we needed some background noise so that she wouldn’t wake up and start whining whenever someone rolled over or made some small sound.  So we started using a Google  Home device to play “Forest Sounds” at night.  Over time, we became used to this, and found it filters out street noises and other nighttime distractions.</p>

<p>With this new setup, I was looking for a way to have a completely automated, “hands-free”, solution that would turn on Groove Salad in the morning, and turn on some kind of forest sounds at night.  Something that just worked, and that I didn’t have to even think about.</p>

<p>Mopidy has a REST API that you can use to control it remotely via HTML POST transactions.  It’s not particularly well documented. At least, it looks like documentation was started, but never completed.  However, there is enough there to figure out how to do some interesting and useful things.</p>

<p>In order to investigate how to use the API, on my workstation I downloaded an application called, “Bruno”, which is an open source alternative to “Postman”.  This application allows you to create REST API calls that you can test and see the results in real time.</p>

<p>That’s what I did with Mopidy.  I poked around trying various API calls to see what would happen, and to figure out the correct syntax and structure.  Eventually, I had a number of tests written that would control the Mopidy server, and that would start and stop both GrooveSalad and my forest sounds MP3.</p>

<p>One of the nice things about Bruno is that you can export the tests as stand-alone Bash scripts (as well as code in a few programming languages).  I copied them out and then put them in <code class="language-plaintext highlighter-rouge">/usr/local/bin/mopidy</code> on the Mopidy/Snapcast LXC. Let’s have a look at a couple of them:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">cat </span>clearTrackList
curl <span class="nt">--request</span> POST <span class="se">\</span>
  <span class="nt">--url</span> http://mopidy.mydomain:6680/mopidy/rpc <span class="se">\</span>
  <span class="nt">--header</span> <span class="s1">'content-type: application/json'</span> <span class="se">\</span>
  <span class="nt">--data</span> <span class="s1">'{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "core.tracklist.clear"
}'</span>
</code></pre></div></div>
<p>This script clears the tracklist out of Mopidy.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">cat </span>addGrooveSalad
curl <span class="nt">--request</span> POST <span class="se">\</span>
  <span class="nt">--url</span> http://mopidy.mydomain:6680/mopidy/rpc <span class="se">\</span>
  <span class="nt">--header</span> <span class="s1">'content-type: application/json'</span> <span class="se">\</span>
  <span class="nt">--data</span> <span class="s1">'{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "core.tracklist.add",
  "params": {
    "uris": [
      "somafm:channel:/groovesalad"
    ]
  }
}'</span>
</code></pre></div></div>
<p>This puts the GrooveSalad channel from SomaFM into the playlist.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">cat </span>startPlayback
curl <span class="nt">--request</span> POST <span class="se">\</span>
  <span class="nt">--url</span> http://mopidy.mydomain:6680/mopidy/rpc <span class="se">\</span>
  <span class="nt">--header</span> <span class="s1">'content-type: application/json'</span> <span class="se">\</span>
  <span class="nt">--data</span> <span class="s1">'{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "core.playback.play"
}'</span>
</code></pre></div></div>
<p>This script starts the playback.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">cat </span>startGrooveSalad
/usr/local/bin/mopidy/clearTrackList
/usr/local/bin/mopidy/addGrooveSalad
/usr/local/bin/mopidy/startPlayback
</code></pre></div></div>
<p>Finally, this one calls the other three back-to-back to start GrooveSalad playing.</p>

<p>How to run these? Well, <code class="language-plaintext highlighter-rouge">cron</code> of course! [Note: On a later project that I used ChatGPT to troubleshoot, it told me that <code class="language-plaintext highlighter-rouge">cron</code> is old-fashioned and that there is a more modern approach using <code class="language-plaintext highlighter-rouge">systemd</code>.  You may want to pursue that, but I find that <code class="language-plaintext highlighter-rouge">cron</code> works just fine.  What can I say?  I’m a dinosaur.]</p>

<p>I’m not going to go into <code class="language-plaintext highlighter-rouge">cron</code> in depth, but for those of you who have never heard of it, <code class="language-plaintext highlighter-rouge">cron</code> is a service that will run whatever you want on a schedule that you set up.  You can specify certain days of the week or of the month, certain minutes of the hour or particular hours that you want your action to run.</p>

<p>I needed two actions, one to start up GrooveSalad in the morning, and one to start up the forest sounds at night:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">sudo </span>crontab <span class="nt">-l</span>
<span class="c"># Edit this file to introduce tasks to be run by cron.</span>
<span class="c">#</span>
<span class="c"># Each task to run has to be defined through a single line</span>
<span class="c"># indicating with different fields when the task will be run</span>
<span class="c"># and what command to run for the task</span>
<span class="c">#</span>
<span class="c"># To define the time you can provide concrete values for</span>
<span class="c"># minute (m), hour (h), day of month (dom), month (mon),</span>
<span class="c"># and day of week (dow) or use '*' in these fields (for 'any').</span>
<span class="c">#</span>
<span class="c"># Notice that tasks will be started based on the cron's system</span>
<span class="c"># daemon's notion of time and timezones.</span>
<span class="c">#</span>
<span class="c"># Output of the crontab jobs (including errors) is sent through</span>
<span class="c"># email to the user the crontab file belongs to (unless redirected).</span>
<span class="c">#</span>
<span class="c"># For example, you can run a backup of all your user accounts</span>
<span class="c"># at 5 a.m every week with:</span>
<span class="c"># 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/</span>
<span class="c">#</span>
<span class="c"># For more information see the manual pages of crontab(5) and cron(8)</span>
<span class="c">#</span>
<span class="c"># m h  dom mon dow   command</span>
30 7 <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> /usr/local/bin/mopidy/startGrooveSalad
0 21 <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> /usr/local/bin/mopidy/startNatureSounds
</code></pre></div></div>
<p>I’ve left in the default comments so that it will look like what you’ll see if you do this on your own system.</p>

<p>The easiest way to edit <code class="language-plaintext highlighter-rouge">/etc/crontab</code> is to just type <code class="language-plaintext highlighter-rouge">sudo crontab -e</code> and it’ll get you there. We need this to run as <code class="language-plaintext highlighter-rouge">root</code>, so <code class="language-plaintext highlighter-rouge">sudo</code> is used here.  The command <code class="language-plaintext highlighter-rouge">crontab -l</code> just lists the crontab file.  These two commands run every day, the first one at 7:30 AM, and the second at 9:00 PM.</p>

<h2 id="handling-stream-interuptions">Handling Stream Interuptions</h2>

<p>The last issue that I had was due to connection issues from SomaFM which caused the stream to stop momentarily.  This caused the Mopidy server to throw an error and stop:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Mar 31 15:24:31 snapcast mopidy[25889]: ERROR    [MainThread] mopidy.audio.gst GStreamer error: Internal data stream error.
</code></pre></div></div>
<p>or:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Mar 30 07:30:18 snapcast mopidy[25889]: ERROR    [MainThread] mopidy.audio.gst GStreamer error: Server does not support seeking.
</code></pre></div></div>
<p>I wish that Mopidy or its SomaFM plugin would be more robust to these disruptions, or have some self-recovery feature built in, but it does not. That meant it was up to me to figure out how to detect these stoppages and restart the stream.</p>

<p>The first thing I did was to look for something in the API that would tell me that the stream had stopped.  I was hopeful that the “status” command would be the answer.  Unfortunately, when I tested this I found that the status was still listed as “playing”, from which I assume that the error that stopped stream wasn’t causing it to go into some elegant shutdown that updated the status.</p>

<p>Luckily, I had decided to connect Mopidy to SnapCast via a named pipe.  Perhaps if I checked the last update time of that pipe I could see if the stream had stopped?</p>

<p>This turned out easy to check.  The pipe was at <code class="language-plaintext highlighter-rouge">/var/local/snapfifo</code> and a listing shows that it’s date and time is right now:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ll snapfifo
prw-rw-rw- 1 snapserver snapserver 0 Apr  3 14:54 snapfifo
</code></pre></div></div>
<p>If I stop the stream, then the date/time stops updating.  So it looks like this is going to be an answer.  I came up with this script:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">cat </span>checkPlaybackState
<span class="c">#!/bin/bash</span>
<span class="k">if</span> <span class="o">(((</span><span class="sb">`</span><span class="nb">date</span> +%s<span class="sb">`</span> - <span class="sb">`</span><span class="nb">date</span> +%s <span class="nt">-r</span> /var/local/snapfifo<span class="sb">`</span><span class="o">)</span> &lt; <span class="s2">"5"</span><span class="o">))</span>
<span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"</span><span class="sb">`</span><span class="nb">date</span><span class="sb">`</span><span class="s2"> Still going"</span> <span class="o">&gt;&gt;</span> /var/local/playbackcheck
<span class="k">else
  </span><span class="nb">echo</span> <span class="s2">"</span><span class="sb">`</span><span class="nb">date</span><span class="sb">`</span><span class="s2"> Not running"</span> <span class="o">&gt;&gt;</span> /var/local/playbackcheck
  /usr/local/bin/mopidy/startGrooveSalad
<span class="k">fi</span>
</code></pre></div></div>
<p>Eventually, I removed the <code class="language-plaintext highlighter-rouge">echo</code> commands, since they really aren’t needed.  In my running version, I have a line that submits a message to my <code class="language-plaintext highlighter-rouge">Gotify</code> server in the event that it detects that the stream has stopped.</p>

<p>I need to run it throughout the day, which means more <code class="language-plaintext highlighter-rouge">cron</code> to the rescue:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>*/3 8-20 * * * /usr/local/bin/mopidy/checkPlaybackState
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">*/3</code> means every 3 minutes, and the <code class="language-plaintext highlighter-rouge">8-20</code> means between 8:00 AM and 8:00 PM. These hours are inclusive, so the last run will be at 20:57, or 8:57 PM.  Then, at 9:00 PM the forest sounds start and that is entirely local, so there’s no need to check for stream issues.</p>

<h1 id="conclusion">Conclusion</h1>

<p>Success in these kinds of projects is ususally evaluated by how happy my wife is with how it works.  She was getting quite fed up with the Google issues that we were experiencing, and frustrated with the constant drops in the service and times when it simply wouldn’t start up again.</p>

<p>She considers this to be one the biggest successes of my self-hosting project so far!  Hurray!</p>

<p>It’s a success because it just works, and it never forces its way into the foreground by making you fiddle with it to get it to work.  It starts up the music in the morning and the forest sounds at night and you don’t have to even think about it.  It’s just there.</p>

<p>To be sure, there were some hiccoughs to start with…</p>

<p>The Raspberry Pi Zero has a woefully weak WiFi tranceiver in it.  This caused grief until I sorted out procurement and placement of WiFi access points around the house.  Symptoms of this problem were sporadic pauses in the audio of up to 2 seconds.</p>

<p>The SnapCast system seems to be dependent on stable connectivity between the server and the clients.  At first, I had one of my WiFi access points connected to the wired network through a powerline AV connector.  These are notoriously quirky, and can suffer interruptions if someone runs a heavy appliance like a clothes washer or dryer in the house.  Once again, the symptoms were sporadic pauses in the audio.</p>

<p>When I shifted AP connectivity over to MoCa (Ethernet over co-ax), all of the issues with sporadic audio pauses largely vanished.</p>

<p>Now it just runs.  If something goes wrong, it fixes itself.  Most of the time, the only reason I’m even aware that it had a problem is because I get a Gotify alert for it.  But even this only happens rarely.</p>]]></content><author><name>Dave Barrett</name><email>pragmaticcoding8@gmail.com</email></author><category term="homelab" /><summary type="html"><![CDATA[The final installment of a 3 part series describing how to set up whole-home audio using SnapCast, Mopidy and Rasberry Pi's. In this article, we look at how to install and configure Mopidy as a streaming service, and how to connect it to a SnapCast server.]]></summary></entry><entry><title type="html">Whole Home Audio With SnapCast - Part II</title><link href="https://www.pragmaticcoding.ca/homelab/snapcast1" rel="alternate" type="text/html" title="Whole Home Audio With SnapCast - Part II" /><published>2026-06-21T17:00:00+00:00</published><updated>2026-06-21T17:00:00+00:00</updated><id>https://www.pragmaticcoding.ca/homelab/snapcast1</id><content type="html" xml:base="https://www.pragmaticcoding.ca/homelab/snapcast1"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>This is Part II of a series about setting up whole-home audio using SnapCast, Mopidy and RaspberryPi Zero’s (Pi0’s).  <a href="/homelab/snapcast0">Part I</a> gives an introduction to how SnapCast works, how we’re going to use it with Mopidy, and instructions for setting up the SnapCast server.</p>

<p>In this article we’ll look at setting up the Pi0’s as SnapCast clients.</p>

<h1 id="raspberrypi-zeros">RaspberryPi Zero’s</h1>

<p>The RaspberryPi Zero is one of the smallest of the RaspberryPi’s (with the Pico being a bit smaller).  It looks like this:</p>

<p><img src="/assets/homelab/RPi0.jpg" alt="RPi0" /></p>

<p>There’s no banana for scale, but the board is about 6cm long.</p>

<p>It has built-in WiFi, which some of the earlier full-sized Pi’s did not, that adds a little bit more versatility to it.  It also has the full set GPIO pins, just like the larger models.  It’s a bit less powerful than most of the other models, with less memory, but it will do just fine as a SnapCast client.</p>

<p>At the time that I did the installations there were images available for Raspbian based on Debian 12, also known as “Bookworm”.  I didn’t see anything listed for Debian 13, or “Trixie” (there was, and I missed it). So I went with the older Bookworm-based Raspbian.</p>

<p>In these instructions I’m going to stick with the Bookworm, because that’s what I used. However, it is highly likely that you could install Trixie and then use the Trixie compatible version of the SnapCast client software and it would work just fine.</p>

<p>I consider these things, configured as they are, to be essentially “appliances”.  They do a specific job, they’re not exposed to the outside world, and, once they are up and running, I don’t expect to be updated - ever. It is possible that one day there might be some new feature in SnapCast that I absolutely have to have, but I cannot imagine what that might be right now.  They are working fine for me, and I wouldn’t be looking at upgrading just for bug fixes or minor changes.  I certainly wouldn’t update just to move the devices to a newer O/S.</p>

<p>By the same token, it wouldn’t affect my homelab if the SnapCast project was halted and removed from GitHub tomorrow.  My installation would just continue to work as it always had.</p>

<p>There’s really two elements that need to be configured on the RPi to make it work.  The first is the audio hardware, and the second is the SnapCast client, which runs as a service.  By far, the audio hardware was the most difficult to figure out, and required a fair bit of trial and error.  Consequently, I didn’t create a step-by-step guide as I was doing it.  I did end up with a process that I could follow 3 more times after I had figured it out.</p>

<p>For what it’s worth, when setting up the 2nd, 3rd and 4th clients I just SSH’d into the first one and typed <code class="language-plaintext highlighter-rouge">history</code>, then replicated the steps on the new RPi0.</p>

<h2 id="install-the-dac-board">Install the DAC Board</h2>

<p>The RPi0 does not have any analog audio output, so you’ll need an add-on board to do this.  For this project, I’ve used the Pimoroni DAC SHIM because it was readily available and inexpensive.  It’s possible, even if you are using a larger RPi, that you’ll still need an external DAC of some sort to get adequate quality sound.</p>

<p>The Pimoroni SHIM looks like this:</p>

<p><img src="/assets/homelab/audio-dac-shim.webp" alt="Pimoroni SHIM" /></p>

<p>If you are using the Pimoroni board, then orient it such that the 3.5mm jack is over top of the SD card slot on the RPi0.  Then line up the holes with the GPIO pins and push it down, all the way to the bottom.  It should be a tight fit and require a fair amount of pressure to get it to slide down.  But not so much that you would describe it as “forcing” it down.</p>

<p>That’s all it takes.  Some of the HiFiBerry boards have the same kind of connection, so they should be just as easy.  If you’re using something else, then RTFM to figure it out.</p>

<h2 id="install-raspbian">Install Raspbian</h2>
<p>If you are new to Raspberry Pi’s, then you need to understand that they usually boot from an operating system loaded onto an SD card of some sort.  The RPi0’s use MicroSD cards.  8GB or 16GB cards should be more than enough.  Bigger ones work just fine.  The operating system is loaded via some other computer that you have that can read/write to SD cards.</p>

<p>To load the O/S onto the the SD card you should use the official <code class="language-plaintext highlighter-rouge">Imager</code> software.  The instructions for installing it are <a href="https://www.raspberrypi.com/documentation/computers/getting-started.html#raspberry-pi-imager">here</a>.  I’m using Ubuntu on my desktop system, so I was able to just use <code class="language-plaintext highlighter-rouge">sudo apt install rpi-imager</code> and it was all done.</p>

<p>When you run the <code class="language-plaintext highlighter-rouge">imager</code>, the first step is to pick the type of RPi device and then the O/S version.  As of the time of writing this, the correct option was “Legacy”, not “Trixie”.  This should give you the “Bookworm” based Raspbian, which is compatible with the SnapCast client software.</p>

<p>Once you’ve picked the output device, which should be your new MicroSD card, you’ll get an option to edit the settings.  Do that.</p>

<p>Change the hostname to something that makes sense to you.  I picked “snapclient0”, then “snapclient1” for the next one, and so on.</p>

<p>Set a username and a password, make note of both.  Then input the WiFi connection details and set the time zone and locale.</p>

<p>In the “SERVICES” tab, enable SSH and use password authentication.</p>

<p>Click “SAVE”, and then pick “YES”.  Go through the “Are you sure?” warnings and let it run.</p>

<p>When it’s done, stick the MicroSD card in the RPi0 and plug it in the power.  Theoretically at least, in a little while, you should be able to SSH into it.  In practice, I have found that they get stuck on the first boot waiting for some kind of keyboard input.  So you might need to plug in a monitor, keyboard and mouse.</p>

<p>Be warned that the RPi0 only has two USB “B” ports, and one is taken up with the power input.  So you’ll need a USB hub to connect both a mouse and keyboard at the same time.  Also note that those two USB ports are very close together, so if you’re using a bunch of adaptors and converters, you’ll need to make sure that they fit when the power port is populated.</p>

<p>Secondly, the HDMI connector on the RPi0 is a <strong>mini HDMI</strong>, You’ll need a compatible HDMI cable or a converter for that.</p>

<p>I’m going off memory here, but I think that one of the key things you’ll need to do with the KVM hooked up is to tell the RPi0 <strong>not</strong> to load the GUI desktop at boot.  This will speed up subsequent boots and boost your performance.  Also, you’ll never use the GUI environment.</p>

<p>Once you’ve got the RPi0 fully booted, you should be able to SSH into it.  I’d give it one more test boot, just to make sure completes a “hands free” boot, before disconnecting the KVM equipment.</p>

<p>I know these instructions seem a little vague, but there’s nothing difficult or tricky about this part, and  the steps should be obvious as you run through it.</p>

<p>SSH into your RPi0 and start configuring the SnapCast client…</p>

<h2 id="edit-the-firmware-configuration">Edit The Firmware Configuration</h2>

<p>This is how you tell the RPi to use the add-on board and not any native audio equipment.</p>

<p>The file you need to edit is <code class="language-plaintext highlighter-rouge">/boot/firmware/config.txt</code>. I’ve marked the lines that needed to be changed or added:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># For more options and information see
# http://rptl.io/configtxt
# Some settings may impact device functionality. See link above for details

# Uncomment some or all of these to enable the optional hardware interfaces
dtparam=i2c_arm=on  &lt;---- uncomment
dtparam=i2s=on      &lt;---- uncomment
dtparam=spi=on      &lt;---- uncomment

# Enable audio (loads snd_bcm2835)
dtparam=audio=off

# Additional overlays and parameters are documented
# /boot/firmware/overlays/README

# Automatically load overlays for detected cameras
camera_auto_detect=1

# Automatically load overlays for detected DSI displays
display_auto_detect=1

# Automatically load initramfs files, if found
auto_initramfs=1

# Enable DRM VC4 V3D driver
dtoverlay=vc4-kms-v3d
max_framebuffers=2

# Don't have the firmware create an initial video= setting in cmdline.txt.
# Use the kernel's default instead.
disable_fw_kms_setup=1

# Disable compensation for displays with overscan
disable_overscan=1

# Run as fast as firmware / board allows
arm_boost=1

dtoverlay=hifiberry-dac   &lt;--- add this line
gpio=25=op,dh

[cm4]
# Enable host mode on the 2711 built-in XHCI USB controller.
# This line should be removed if the legacy DWC2 controller is required
# (e.g. for USB device mode) or if USB support is not required.
otg_mode=1

[cm5]
dtoverlay=dwc2,dr_mode=host

[all]
gpio=25=op,dh   &lt;--- add this line
</code></pre></div></div>
<p>You will also need a file called <code class="language-plaintext highlighter-rouge">/etc/asound.conf</code>.  You’ll probably have to create it, and it needs to have just two lines:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>defaults.pcm.card 1
defaults.ctl.card 1
</code></pre></div></div>

<h2 id="installing-the-snapclient">Installing the SnapClient</h2>

<p>The next step is to install the SnapClient service software.  Just as with the SnapServer software, it’s best to download it from GitHub and then use <code class="language-plaintext highlighter-rouge">dpkg</code> to install it.  It’s also advisable to make sure that all of your O/S elements are updated before you attempt the installation, so I’ve included those steps in here:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>4  wget https://github.com/snapcast/snapcast/releases/download/v0.34.0/snapclient_0.34.0-1_armhf_bookworm.deb
5  sudo apt update
6  sudo apt upgrade
7  sudo dpkg -i snapclient_0.34.0-1_armhf_bookworm.deb
</code></pre></div></div>
<p>This will install the <code class="language-plaintext highlighter-rouge">snapclient</code> software and set it up as a service.  You should set it up to run at boot:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo systemctl enable snapclient
</code></pre></div></div>
<p>In most versions of Linux, you use <code class="language-plaintext highlighter-rouge">systemctl</code> to control sevices on the system.  The <code class="language-plaintext highlighter-rouge">enable</code> command in <code class="language-plaintext highlighter-rouge">systemctl</code> tells the system to load the service at boot time.</p>

<p>Before you do anything else, you should configure the client.  Edit the file <code class="language-plaintext highlighter-rouge">/etc/default/snapclient</code>.  The file should already be there, and it should look something like this when you are done:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Start the client, used only by the init.d script
START_SNAPCLIENT=true

# Additional command line options that will be passed to snapclient
# note that user/group should be configured in the init.d script or the systemd unit file
# For a list of available options, invoke "snapclient --help"
SNAPCLIENT_OPTS="-h 10.93.50.107 -s 1"
</code></pre></div></div>
<p>Obviously, you’ll need to put in whatever IP address your SnapServer is running on, instead of mine.  The <code class="language-plaintext highlighter-rouge">-s 1</code> is important as it tells the SnapCast client to use the DAC board.</p>

<p>At this time, you should reboot the RPi0, which will cause it to load the new audio configuration and start the SnapClient service.</p>

<h2 id="testing-and-debugging">Testing and Debugging</h2>

<p>If everything has gone well, then your SnapCast server should have received a connection request from your RPi0.  This means that you should see it if you go back into your SnapCast web page.</p>

<p>If you issue the following command on your SnapCast server:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo journalctl -u snapserver -n40
</code></pre></div></div>
<p>You should see a line near the bottom that looks something like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Apr 02 18:28:40 snapcast snapserver[41889]: (Server) Hello from b8:27:eb:a3:3d:60,
         host: snapcast0, v0.34.0, ClientName: Snapclient, OS: Raspbian GNU/Linux 12 (bookworm),
         Arch: armv6l, Protocol version: 2, Auth: {"param":"","scheme":""}
</code></pre></div></div>
<p>If not, then your RPi0 hasn’t connected to SnapCast server.  Back on the RPi0, you should be able to do this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo systemctl status snapclient --lines 30
● snapclient.service - Snapcast client
     Loaded: loaded (/lib/systemd/system/snapclient.service; enabled; preset: enabled)
     Active: active (running) since Fri 2026-04-03 12:46:35 EDT; 3min 16s ago
       Docs: man:snapclient(1)
   Main PID: 17962 (snapclient)
      Tasks: 2 (limit: 373)
        CPU: 32.803s
     CGroup: /system.slice/snapclient.service
             └─17962 /usr/bin/snapclient --hostID SomeRoom --logsink=system -h 10.93.50.107 -s 1

Apr 03 12:46:35 snapcast1 systemd[1]: Started snapclient.service - Snapcast client.
Apr 03 12:46:35 snapcast1 snapclient[17962]: (Snapclient) Options '--host' and '--port' are deprecated. Please add the server URI as last command line argument
Apr 03 12:46:35 snapcast1 snapclient[17962]: (Snapclient) Version 0.34.0, revision 0ac55084
Apr 03 12:46:35 snapcast1 snapclient[17962]: (Connection) Resolving host IP for: 10.93.50.107
Apr 03 12:46:35 snapcast1 snapclient[17962]: (Connection) Connecting to host: 10.93.50.107:1704, port: 1704, protocol: tcp
Apr 03 12:46:35 snapcast1 snapclient[17962]: (Connection) Connected to 10.93.50.107
Apr 03 12:46:35 snapcast1 snapclient[17962]: (Connection) My MAC: "b8:27:eb:7b:b4:33", socket: 9
Apr 03 12:46:36 snapcast1 snapclient[17962]: (Controller) ServerSettings - buffer: 2000, latency: 0, volume: 90, muted: 0
Apr 03 12:46:36 snapcast1 snapclient[17962]: (Controller) Codec: flac, sampleformat: 48000:16:2
Apr 03 12:46:36 snapcast1 snapclient[17962]: (Player) Player name: alsa, device: lavrate, description: Rate Converter Plugin Using Libav/FFmpeg Library, idx: 1, sharing mode: unspecified, parameters: &lt;none&gt;
Apr 03 12:46:36 snapcast1 snapclient[17962]: (Player) Mixer mode: software, parameters: &lt;none&gt;
Apr 03 12:46:36 snapcast1 snapclient[17962]: (Player) Sampleformat: 48000:16:2, stream: 48000:16:2
Apr 03 12:46:36 snapcast1 snapclient[17962]: (Alsa) Using default buffer_time: 80 ms, default fragments: 4
Apr 03 12:46:36 snapcast1 snapclient[17962]: (Alsa) PCM name: lavrate, sample rate: 48000 Hz, channels: 2, buffer time: 80000 us, periods: 4, period time: 20000 us, period frames: 960
Apr 03 12:46:36 snapcast1 snapclient[17962]: (Stream) No chunks available
Apr 03 12:46:36 snapcast1 snapclient[17962]: (Alsa) Failed to get chunk
Apr 03 12:46:36 snapcast1 snapclient[17962]: (Controller) diff to server [ms]: -3.68704e+08
</code></pre></div></div>
<p>You should see that it is active in the “Active:” line.  You should also see that it says “enabled” somewhere in the “Loaded:” line.</p>

<p>You should also see in the “CGroup:” line 2, that you have a host IP address that is correct for your SnapCast server.</p>

<p>Underneath the summary is most recent journal log output since the service has been started.  You should see the interaction between the client and the server happening here.</p>

<p>You can push the white noise through the fifo again, and you should hear it through the RPi0.  Just about anything that you can plug into a 3.5mm jack should work.  A powered speaker, an amplifier, even headphones.</p>

<h2 id="what-if-you-dont-hear-anything">What If You Don’t Hear Anything</h2>

<p>If you saw the client connecting to the server, but you don’t hear anything.  Then you have a problem with your sound configuration or you haven’t included that “-s 1” in the client options.  Check that last bit first.</p>

<p>You can confirm the sound devices with <code class="language-plaintext highlighter-rouge">aplay</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>aplay <span class="nt">-l</span>
<span class="k">****</span> List of PLAYBACK Hardware Devices <span class="k">****</span>
card 0: vc4hdmi <span class="o">[</span>vc4-hdmi], device 0: MAI PCM i2s-hifi-0 <span class="o">[</span>MAI PCM i2s-hifi-0]
  Subdevices: 1/1
  Subdevice <span class="c">#0: subdevice #0</span>
card 1: sndrpihifiberry <span class="o">[</span>snd_rpi_hifiberry_dac], device 0: HifiBerry DAC HiFi pcm5102a-hifi-0 <span class="o">[</span>HifiBerry DAC HiFi pcm5102a-hifi-0]
  Subdevices: 0/1
  Subdevice <span class="c">#0: subdevice #0</span>
</code></pre></div></div>
<p>That “card 1” should line up with your “-s 1”.  If the DAC has some different number then use it for the “-s” option.</p>

<p>It took me a lot of fiddling about to find out how to get the setup right on the very first RPi0 that I set up, because I <strong>didn’t</strong> get any sound at the beginning.  Looking back at my history stack from that first RPi0, I see that <code class="language-plaintext highlighter-rouge">aplay</code> was really the tool that let’s you figure it out.</p>

<p>You can use <code class="language-plaintext highlighter-rouge">aplay</code> to play a file.  This let’s you eliminate the SnapCast elements and confirm that, yes, your RPi0 is sending sound through the DAC:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>wget https://sample-files.com/downloads/audio/wav/pure-tone.wav
<span class="nv">$ </span>aplay pure-tone.wav
</code></pre></div></div>
<p>Take a good look at the man pages for <code class="language-plaintext highlighter-rouge">aplay</code>.  There are lots of different options you can play with to understand what’s going on with you sound system.</p>

<h1 id="conclusion">Conclusion</h1>

<p>At this point you should have a Raspberry Pi Zero running as a SnapCast client that you can send sound to from a SnapCast server!</p>

<p>I can say that the first one took me a couple of days to get running because the documentation just isn’t there, and there are quite a few moving parts to figure out at the beginning.  However, if you are using a RPi0 and a HiFiBerry or Pimoroni DAC there is no reason that you need to have go through the same experience because all of the details are right here in this article.</p>

<h2 id="this-is-actually-easy">This is Actually Easy</h2>

<p>I’ve now set up 4 of these RPi0’s and it takes about half an hour all-in, including the 15 minutes or so that the Imager program takes to set up the SD card.  Here is the entire history stack from the 4th one:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1  ip a
2  <span class="nb">sudo </span>apt update
3  <span class="nb">sudo </span>apt upgrade
4  <span class="nb">sudo </span>vi /boot/firmware/config.txt
5  wget https://github.com/snapcast/snapcast/releases/download/v0.34.0/snapclient_0.34.0-1_armhf_bookworm.deb
6  <span class="nb">sudo </span>dpkg <span class="nt">-i</span> snapclient_0.34.0-1_armhf_bookworm.deb
7  <span class="nb">sudo </span>systemctl status snapclient
8  <span class="nb">sudo </span>vi /etc/default/snapclient
9  <span class="nb">sudo </span>systemctl restart snapclient
10  <span class="nb">sudo </span>systemctl status snapclient
11  aplay <span class="nt">-l</span>
12  <span class="nb">sudo </span>reboot
13  aplay <span class="nt">-l</span>
14  <span class="nb">sudo </span>systemctl status snapclient
15  <span class="nb">sudo </span>vi /etc/asound.conf
16  <span class="nb">sudo </span>reboot
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">ip a</code> at the beginning was to make it easier for me to find the device in my DHCP leases and set the address as static there.</p>

<p>After line 10, I don’t think it was playing anything when I plugged it into the amp.  Then you can see me checking <code class="language-plaintext highlighter-rouge">aplay -l</code>, rebooting it to make sure it had reread the configuration, then checking again.  I finally realized that I had forgotten to create <code class="language-plaintext highlighter-rouge">/etc/asound.conf</code>, so I created it.  Then another reboot, and sound was coming out of it.</p>

<p>At that point I disconnected from it and haven’t log in again until I started writing this article.</p>

<h2 id="in-part-iii">In Part III…</h2>

<p><a href="/homelab/snapcast2">Part III</a> is the final article in the series, and shows how to set up Mopidy to stream music and how to connect it to your SnapCast server.  It also shows how you can use the Mopidy REST API to do some simple automation and stream recovery.</p>]]></content><author><name>Dave Barrett</name><email>pragmaticcoding8@gmail.com</email></author><category term="homelab" /><summary type="html"><![CDATA[Part 2 of a 3 part series describing how to set up whole-home audio using SnapCast, Mopidy and Rasberry Pi's. In this article, we look at how to configure a Raspberry Pi Zero as a SnapCast client.]]></summary></entry><entry><title type="html">Lenovo M910Q Tiny Homelab Server</title><link href="https://www.pragmaticcoding.ca/homelab/lonovo_tiny" rel="alternate" type="text/html" title="Lenovo M910Q Tiny Homelab Server" /><published>2026-06-20T17:00:00+00:00</published><updated>2026-06-20T17:00:00+00:00</updated><id>https://www.pragmaticcoding.ca/homelab/Lenovo-M910Q</id><content type="html" xml:base="https://www.pragmaticcoding.ca/homelab/lonovo_tiny"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>When I initiated my project to build a homelab, I didn’t want a huge up-front expense.  But I did have a need for whatever I built to be as reliable as possible.  After all, I’m planning on replacing a fair number of cloud services - services that are generally pretty reliable - with home-hosted alternatives.</p>

<p>To me, this means redundancy.</p>

<p>I’ve looked at lots of photos of people’s homelab setups and good number of them, probably even the majority of them, seem to have one server doing most of the work.  This is, of course, a single point of failure.  In my book, that’s just not something that you do.</p>

<p>Proxmox is a great help here.  While setting up high availability - where you set up a cluster of hosts, designate vital containers and VM’s, and then have them fail over to different hosts when their host fails - is difficult without having lots of fast disk space and multi-gigabit networking, you can still use backups to restore vital servers when a host fails.</p>

<p>But, how do you create an effective cluster of servers on a shoestring budget without turning your paycheck over to the electic company?</p>

<h2 id="1-litre-pcs">1 Litre PC’s</h2>

<p>It wasn’t long after I started thinking about building a homelab that I started seeing videos, posts, and articles about “mini” PC’s that people were using.  These were tiny little desktop computers that idled at about 7W and could handle a surprising amount of load.</p>

<p>It turns out that there are three basic product lines.  HP has an EliteDesk series that they call “Mini”, Dell has an Optiplex line called “Micro” and Lenovo has a ThinkCentre line called “Tiny”.  I’m not going to say that they are the same, but, they are all pretty close.  These are all what are called “1 Litre” form factor PC’s, and they all use external power bricks instead of internal power supplies.  A common term referring to these computers collectively is “Tiny Mini Micro”.</p>

<p>Here’s what the Lenovo version looks like, with a pen so that you can see the scale:</p>

<p><img src="/assets/homelab/M910Q-Front-Back.png" alt="M910Q" /></p>

<p>You can see that it’s pretty darned small.</p>

<p>These units were incredibly popular with corporate clients.  They bought gazillions of these things because they just worked well for office applications, they were sturdy, and they were cheap.  A lot of them were leased.</p>

<p>And when those leases ended…</p>

<p><img src="/assets/homelab/M910Q-stack.jpg" alt="M910 Stack" /></p>

<p>The leasing companies sold them off in bulk at wholesale prices to recover some of the value, and the companies that bought them have refurbished them and are reselling them.  The result is that they are readily available at reasonable prices.</p>

<p>I was told that the advent of Windows 11 has caused a shortage of these units, and the prices have gone up a fair bit.  Even so, they are still a great value.</p>

<h2 id="why-lenovo">Why Lenovo</h2>

<p>In truth, you probably cannot go wrong with either of the three brands.  These computers were all designed for the same, commercial, market and they are all executed well.  In my area, I was seeing a lot of Dells and Lenovos available, but not so many HPs.  I went with Lenovo because they seemed just a little bit more solid, and there was a bit more variety in the models available.</p>

<p>One thing I really wanted was to have all of the units in the cluster as similar as possible.  I wanted them to be fungible.  As I was planning to purchase them over time, I needed the supply to be consistent for at least a while.</p>

<p>You should also know that these computers, are still manufactured today and available to purchase new, although these are newer versions with more power and a higher cost..</p>

<h1 id="the-hardware">The Hardware</h1>

<p>Let’s take a look at these boxes and see what you get.</p>

<p>These have been in production for quite some time.  You’ll be able to find models like the M73Q which have 4th gen Intel Core processors for peanuts, but those are probably not going to have enough computing power to be useful.  I decided on the Lenove M910Q which has 6th or 7th generation Core processors.</p>

<p>You do need to be a little careful.  All of these computers look identical, so you have to make sure that you are getting what you think you are getting and that you know what kind of processors go in which models.  This product line is also still in production, so you can buy more recent versions with 10-12th generation Intel processors - although at a higher price.</p>

<p>Beyond that, they all come with a variety of different available processors.  So, once again, be careful to make sure that you are purchasing what you think you are.  Don’t pay an i7 price for an i3 equipped unit.</p>

<h2 id="m910q-vs-m710q">M910Q vs M710Q</h2>

<p>If you look into these, you’ll see that there are two models which look and seem to be very, very similiar; the M910Q and the M710.  What’s the difference?</p>

<p>You can see for yourself the spec-sheets:</p>

<p><a href="/assets/homelab/ThinkCentre_M910_Tiny_Spec.PDF">M910 Specs</a></p>

<p><a href="/assets/homelab/ThinkCentre_M710_Tiny_Spec.PDF">M710 Specs</a></p>

<p>As far as I can tell, these two units are nearly the same, except that the M910Q uses the Intel Q270 chipset instead of the B250 chipset in the M710Q.  This means that the M910Q supports VPRO, while the M710Q does not.</p>

<p>Both models support the same set of processors and memory.</p>

<p>The place where I bought mine made no price distinction between the M710Q and the M910Q because the average customer wasn’t building homelabs.  What they cared about was the CPU.  The 7th gen Core i7 cost about double the 6th gen Core i5 models. The price of a system with the same CPU was the identical between the M710Q and the M910Q.</p>

<h2 id="the-processors">The Processors</h2>

<p>The M910Q will take a variety of Intel laptop CPU’s from the 6th and 7th generations.  It will support Core i3, i5 and i7 processors from both generations.  Let’s take a look at the differences, while ignoring the i3.</p>

<p>The i5-7500T is about 10% faster than the the i5-6500T for multithreaded applications, and 8% faster for single-core applications.  Both support 4 cores and 4 threads.  You see the slightly smaller differences between the i7-6700T and the i7-7700T.</p>

<p>Of course the i7-6700T supports 8 threads, but the i7-6700T is just 16% faster than the i5-6500T for single-core performance.  Multi-threaded performance is significantly higher.</p>

<p>The biggest difference is between the i5-6500T and the i7-7700T.  Even here, the scores for Cinebench Single-Core come in at 132cb vs 168cb, while Cinebench Mulit-core is 464cb v 805cb.  This last comparison is the biggest of all and probably significant for Proxmox host systems.</p>

<p>I’m not saying these performance differences are insignificant, but a Core i5-6500T went for about $50 CAD on eBay, while a Core i7-7700T from the same seller went for $120 CAD at the time that I bought my systems.  I was seeing this difference in the prices of the M910Q’s in the store as well.</p>

<p>About 9 months later, you can buy i7-7700T CPU’s, the most powerful CPU’s compatible with these systems, for between $50-$70 (CAD) on eBay.  That’s less than a stick of 16GB RAM for one of these.  This means that upgrading is a very reasonable possibility.</p>

<h2 id="memory">Memory</h2>

<p>The M910Q takes DDR4 laptop style, non-ECC SODIMM’s.  There are two slots, and each can take up to 16GB of RAM, for a total of 32GB.</p>

<p>Most of the systems that I have seen for sale, on line or in person, are loaded with a single 8GB stick of RAM.  I upgraded all of mine to 16GB when I bought them.</p>

<p>I’m kicking myself now, because that extra 8GB cost me about $25 and I could have taken them up to 32GB for about $50.  As I write this, each additional stick of 16GB DDR4 is going to cost me about $75.</p>

<p>Of all of the resources that I worry about being a constraint after implementing quite a few services, it’s RAM.  I’m nowhere close to getting critical, but I expect it will be the first thing to cause a problem.</p>

<h2 id="a-look-inside">A Look Inside</h2>

<p>One of the cool things about the M910Q is that it just needs a single screw on the outside removed in order to gain access to the inside, and most maintenance thereafter is tooless.  The top cover slips off, and then this allows a plate on the bottom to also be removed.</p>

<p>Let’s look at the view of the inside from the top:</p>

<p><img src="/assets/homelab/M910Q-top.jpg" alt="M910Q Inside Top" /></p>

<p>The CPU is socketed, and hidden away under the duct extending back from the fan.  From the videos I’ve seen, I believe that you might need to undo some screws to remove the heat sink and access the CPU socket.</p>

<p>The black plastic construct to the right is the SATA drive cage.  Squeeze that semi-circular plastic bar towards the cage and the whole thing pops out.  The ribbon cable for the drive is held onto the cage with that yellow tape.  On mine, it was zip-tied - which I think is the standard.</p>

<p>The cage will accept a 2.5” drive with a height of 7mm.  You can install an SSD or an HDD.</p>

<p>Underneath it is an M.2 slot for a WiFi card, or something compatible with it.  Some models come with the WiFi card and the antenna.</p>

<p>Pull off the plate on the bottom, and this is what you’ll see:</p>

<p><img src="/assets/homelab/M910Q-bottom.jpg" alt="M910Q Inside Bottom" /></p>

<p>Don’t be fooled by the text on the PCB that says, “SATA/NVMU/SSD2”.  It’s unpopulated on the M910Q and the M710Q.  I believe that the M920Q and the M720Q might have that slot.  In this picture, we have an SSD installed in slot #1.</p>

<p>The only other things under here are slots for the memory.</p>

<p>That’s pretty much it. There are a small number of options that might be installed in these.  Mine have two DB9 serial ports, but others have additional DisplayPort ports.  Presumably, you could swap these out, but the choices are fairly limited.</p>

<p>From what I have seen, some of the later HP EliteDesk Mini’s have a optional 2.5GB adapter on a daughterboard that can just be slotted in.  But there’s nothing like that on the Lenovo’s.</p>

<p>In one of my units, I’ve installed a Coral TPU in the WiFi M.2 slot.  This is for some AI processing in my NVR server.</p>

<h2 id="ports-on-the-backfront">Ports on the Back/Front</h2>

<p>Lenovo sold these in all kinds of configurations, but they all have a pantload of USB 3.1 Gen 1 ports on the back and front.</p>

<p>Most of them seem to have 2 or more DisplayPort ports.  No HDMI.  You might miss this if you’re not paying attention, and you’ll have to make sure that you have a DisplayPort compatible monitor (or a converter) in order to do the initial setup.</p>

<p>They use a standard Lenove square power port.  This is the same as what you’d find on a Lenovo laptop.  They use power bricks that come in 60W and 90W variants.  You’ll probably be fine with the 60W versions for most purposes.</p>

<p>There is but a single 1GB ethernet port.</p>

<p>Mine must have come from some kind of lab or engineering environment.  They are festooned with DB9 serial ports, which I assume were used to connect to test equipment or other stuff that pre-dated USB.  I don’t think I’ll ever use them.</p>

<h1 id="limitations">Limitations</h1>

<p>Without a doubt, the biggest limitation of these systems, if you are going to use them as Proxmox hosts, is that they only have a single 1GB Ethernet adapters.  There’s no upgrade potential here that doesn’t involve soldering and 3D printing.</p>

<p>This means that Ceph, and therefore HA clustering in Proxmox, is probably a no-go.  However, I am planning on seeing how they perform with 2.5Gbs or 5Gbs USB NICs forming a isolated network for the cluster.  It seems that these adapters are getting more stable now.</p>

<p>In general, however, beyond adding memory, a SATA drive and possibly putting in a larger SSD in the NVME slot, there’s not a lot of room for upgrading or enhancing these systems.  There’s just no room inside them.</p>

<p>If you really, really, really feel like you need the ability to add a PCI card to do something specific, then you might consider the M920Q or the M720Q.  These have a PCI slot, although you’ll need an additional riser card to be able to use it.  Note that you’ll probably have to spend about $100-$150 more to get one of these tricked out with the riser card.</p>

<h1 id="pricing">Pricing</h1>

<p>This is where these little units really shine.  The price.</p>

<p>The outfit I bought my units from was selling them with a standard configuration of 8GB RAM and a 256GB M.2 SSD.  The first two ones I ended up with had DB9 serial connectors, but no WiFi.</p>

<p>The basic model with a 6th gen i5 was $100, while the i7 (not sure of the generation) was close to $200.  I did end up with one 7th gen i5, and that was $110, the other two were 6th gen i5 processors. I increased the memory in all of them to 16GB, because I strongly suspected that 8GB wasn’t going to cut it for Proxmox servers.  When I bought the first two, I simply bought an extra 16GB DIMM and them moved one of the 8GB DIMMs to the other system.  For the third one, I just paid to swap out the 8GB DIMM for a 16GB one.</p>

<p>This is going to make upgrading them to 32GB a bit more expensive, because I’ll have to by 4 16GB DIMMs and hope I can trade in the two 8GB DIMMS.</p>

<p>With the memory upgrade, I paid about $115 CAD each for the first two, and then about $145 CAD for the third.  So, about $375 CAD together.  They came with a six month warranty.</p>

<h2 id="value-compared-to-raspberry-pis">Value Compared to Raspberry Pi’s</h2>

<p>I checked, and a RPi 5 with 4GB RAM goes for about $105 CAD.  The performance of the RPi is significantly lower than that of the Core i5-6500T, especially when it comes to multi-core performance, where the RPi scores about 60% of that of the i5-6500T.</p>

<p>It’s almost certain that the M910Q draws more power than the RPi 5, especially when idle.  So if you do have an application that needs fairly low single-core processing power which is idle for most of the time, an RPi might be a more appropriate solution.  Also, if you do have a need for the GPIO capabilities of the RPi, then it’s clearly the way to go.</p>

<h1 id="performance">Performance</h1>

<p>Performance issues are a significant difference between running a corporate data centre and a homelab.</p>

<p>In the corporate world, you can reasonbly assume that all of your various servers are going to be running under a consistent and considerable load all day.  With hundreds of users and services exposed to the Web, you can be pretty sure that just about everything is doing something at any given time.  Especially between 9am and 5pm.  This means that you have to provision and configure your data centre to meet that fairly predicatible load across a number of services active at the same time, and then also to handle peaks that happen from time to time.</p>

<p>But homelabs aren’t like that.  You probably have more virtual servers than users, to start with.  Some of your services are likely to be 99% idle most of the time.</p>

<p>That’s what it’s like with my homelab.  I have a server that streams whole-home audio 24/7, but it only uses 2% of 1 core, and about 60MB of RAM.  I have a couple of servicers that wake up once every 5 minutes and check my two ISP connection speeds, log the results and send me a notification if either one runs really slow.  I have a service that scans my network for active devices every few minutes.</p>

<p>All of the other servers support “on demand” services.  JellyFin is mostly active when someone is watching a video.  Immich when someone is uploading, downloading or viewing pictures.  Vaultwarden when someone is synching passwords.  The backup servers do all of their work in the middle of the night, around 3:00am.  You get the idea.</p>

<p>This has a very different impact on how your servers perform…</p>

<h2 id="cpu">CPU</h2>

<p>One of the biggest surprises for me was just how little CPU the various Proxmox containers actually consume.  I have about 28 VM’s and LXC’s running right now and the Proxmox dashboard says I’m using 4% of my CPU.  These containers include HomeAssistant, JellyFin, SnapCast, NetAlertX, Open Media Vault, Immich, an *arr suite, NGINX and two Technitium DNS servers.  A couple of containers are running Docker.</p>

<p>Once in a while I can peg the CPU on one of the servers.  If I upload a ton of images into Immich, it’ll run hard for a while as it does its facial recognition and processing.  Open Media Vault can get pretty heavy at times, too.</p>

<p>The key thing here is that any given virtual service, running full tilt, is only going to be able to occupy 33% of my homelab’s CPU, because it only has access to the CPU on its own host.  On the other hand, it’s probably a good idea to spread your potentially CPU intensive virtual servers across your Proxmox nodes.  Maybe even consider which ones are likely to be running at the same time, and keep them on different nodes.</p>

<h2 id="memory-1">Memory</h2>

<p>Overall, the cluster seems to be running with about 30% of its memory in use.  This can vary quite a bit if any of the services gets busy.  Most of my servers are deployed as LXC’s which are much more dynamic in the way that they consume and release memory than traditional VM’s.  I think that this has helped to keep the memory requirements fairly low.</p>

<p>Once again, virtual servers cannot access resources not on their host node.  Looking to keep memory intensive applications spread across all the nodes is probably a good idea.</p>

<h2 id="power-consumption">Power Consumption</h2>

<p>Eventually I got around to installing a UPS for my HomeLab and it has a display that shows how much power it’s drawing.  With all of the essential components to stay “up” connected to the UPS, it’s pulling about 80 watts.  This is 3 M910Q’s, the HP T740 hosting the firewall, the external drive bay, the switch and the two modems.</p>

<p>80 watts.  I’m impressed.</p>

<p>The UPS predicts it can last for a couple of hours with that load, and it’s not a big UPS.</p>

<h2 id="application-performance">Application Performance</h2>

<p>In terms of user experience, there are no performance issues.  These servers are 100% capable of meeting every demand placed on them in response to user activity.</p>

<p>One of the things I did when setting up Immich for my wife was to use Google TakeOut to download about 60GB of photos from her Google account.  Then I uploaded all of them into Immich.  I found that it took about 15 minutes for Immich to fully digest about 1000 photos, which included its facial recognition.  This also seemed to line up with the maximum number of files that the upload selection window could handle, which is more of a UI issue than anything else.  So it did take some hours of going back and uploading the next batch of 1000 every 15 minutes or so.  During those 15 minutes, the Immich server was running at about 90% CPU use, so I was too chicken to try to upload another batch of 1000 before it was done.</p>

<p>That was the only time I’ve noticed any impact of the server power on user experience.</p>

<h2 id="after-installing-frigate">After Installing Frigate</h2>

<p>I have recently installed a Frigate NVR virtual server processing two cameras outside my house.  I’m going to be adding a video doorbell to the load on this server.  I added a Coral TPU to the node it’s running on to assist with some of the object detection and recognition that it does.  Additionally, I installed Frigate on the only node that I had that has an internal SATA drive, and that’s an HDD, not an SSD.  I assume that the HDD was idle and not consuming much of any power prior to configuring Frigate to use it as storage for its recordings.</p>

<p>This was the one application that I was a little concerned about with servers this low powered.  However, it turns out not to be a problem.</p>

<p>I now see my entire cluster running at about 20% CPU, with the node hosting Frigate running at about 50% most of the time.  Memory usage on both the cluster and the Frigate node is averaging about 50%.</p>

<p>More than anything else, my UPS is now reporting around 100W consumption most of the time.  That’s an increase of around 20W, which I assume can be attributed to the increased CPU usage and the HDD drive constantly spinning now.</p>

<p>I’m not noticing any peformance degredation in any of my services.  I do however occasionally hear some fan noise coming from that node, that I never heard before.  Otherwise, these servers are virtually silent.</p>

<h1 id="conclusion">Conclusion</h1>

<p>It’s been about 9 months since I bought these servers, and I’ve been constantly adding more and more services to my homelab throughout that time.  At no point in time have I encountered any application where they weren’t up to the task.  These servers are now doing real work all day, and have replaced a bunch of services that I used to have outsourced in the cloud.</p>

<p>That being said, I don’t think that these units are going to be a good fit for hosting my own AI services, which is something I think I’ll probably be looking to do at some point in the future.  But even then I would probably keep my existing cluster and add on something specifically for AI processing.</p>

<p>The one thing that they simply won’t work for, due to the single 1Gb ethernet port, is as a firewall.  You need two ethernet adaptors for that.  So, for a firewall I implemented an HP T740.  It’s very similar to the M910Q’s, but a little bit bigger, has an AMD processor and comes with a PCI slot with a riser card.  This means that I was able to add a 2 port ethernet card to it.</p>

<p>If you are just starting out with self-hosting, I think these are an amazing place to begin.  You can get a single server for less than $200, even with today’s RAM prices, and it will do whatever you need.  Add a second or third, and you have a real data centre with all the possiblities of redundancy that you get along with that.</p>

<p>For myself, I’m really pleased at how these servers have worked out.  I had the goal of building out my homelab over time and within our monthly discretionaly budget.  The low cost of these servers has meant that I have been able to do this and I don’t feel that I’ve sacrificed anything with this approach.</p>]]></content><author><name>Dave Barrett</name><email>pragmaticcoding8@gmail.com</email></author><category term="javafx" /><summary type="html"><![CDATA[A look at the Lenovo M910Q Tiny computers I've chosen as homelab servers.]]></summary></entry><entry><title type="html">Whole Home Audio With SnapCast - Part I</title><link href="https://www.pragmaticcoding.ca/homelab/snapcast0" rel="alternate" type="text/html" title="Whole Home Audio With SnapCast - Part I" /><published>2026-05-20T17:00:00+00:00</published><updated>2026-05-20T17:00:00+00:00</updated><id>https://www.pragmaticcoding.ca/homelab/snapcast0</id><content type="html" xml:base="https://www.pragmaticcoding.ca/homelab/snapcast0"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>For a number of years now, we’ve been using our Google Home/Nest devices to stream music around the entire house.  It’s good when it works, but we’ve found that the music will intermittently just stop playing for no apparent reason.  This is guaranteed to happen several times a day.  I’ve seen that others have reported the same problem.</p>

<p>We play a station from the SomaFM streaming service called “Groove Salad” pretty much all day long.  It’s ambient electronic music which is a good background to day-to-day living for us.  Just as you would expect, we don’t like every single song that they play, but some we like a lot, and most just fade into the background.</p>

<p>Google does a pretty good job of synchronizing the various devices, so if you do find yourself in a spot where you can hear two of them, it doesn’t sound like a weird echo.  I’ve even created a custom command to start the music in the group that we’ved defined for the whole-house music.</p>

<p>That turns out to be worthwhile, because it stops so often.</p>

<p>Additionally, there was a time in early 2024 when our audio chromecast device stopped working for a while.  It turns out that the security certificate that they set up for in on Google’s servers expired after 10 years.  It took a few days for them to sort that out, but the incident showed how dependent we were on Google’s infrastructure and ongoing support for the devices we were using.  It’s not like Google has never suddenly cut support for a product before, either.</p>

<p>I wanted some way to stream the music with the same high quality synchronization without using Google.  I also didn’t want to trade Google for a lock-in on some other proprietary solution like Sonos. I was hoping to find something that was open source, and would work with a variety of hardware.</p>

<p>It turns out that there is a software option out there called “SnapCast” that will do exactly this.</p>

<h1 id="part-one-of-a-series">Part One of a Series</h1>

<p>I started out writing this as a single article, but it quickly became a monster that Jekyll said would take over half an hour to read.  I found myself reluctant to break things down into more sections or to add more information because it was so long already.</p>

<p>I realize that breaking it up into parts doesn’t make it any shorter, but it does make it a little less daunting to read.</p>

<p>In this part you’ll find an introduction to SnapCast, and then instructions for setting up and testing a SnapCast server of your own.  In Part II, we’ll look at setting up Raspberry Pi Zero’s to act as SnapCast clients, and then in the last part we’ll look at setting up Mopidy to stream music and how to connect it to SnapCast.</p>

<p>Let’s get started…</p>

<h1 id="snapcast">Snapcast</h1>

<p>I cannot do better than the description from the Snapcast GitHub page:</p>

<blockquote>
  <p>Snapcast is a multiroom client-server audio player, where all clients are time synchronized with the server to play perfectly synced audio. It’s not a standalone player, but an extension that turns your existing audio player into a Sonos-like multiroom solution.<br />
Audio is captured by the server and routed to the connected clients. Several players can feed audio to the server in parallel and clients can be grouped to play the same audio stream.</p>
</blockquote>

<p>There’s also a diagram which shows how Snapcast works:</p>

<p><img src="/assets/homelab/SnapCastDiagram.png" alt="Snapcast Diagram" /></p>

<p>To sum it up: You set up a server with some sort of music playing software, then install the Snapcast server software and configure it such that it takes its input from the output of the music playing software.  Then you set up Snapcast clients, and configure them to connect to the Snapcast server.  After that, you can stream the music from the music player software on the server to all of the Snapclast clients and they will be synchronized.</p>

<p>In terms of audio quality, I find it to be excellent.  I think the concept of “whole-home audio”, by definition, isn’t expected to be audiophile quality.  It’s designed to be background noise.  But I’ve found this setup to go far beyond that basic requirement.</p>

<h2 id="snapcast-server">Snapcast Server</h2>

<p>As this article is part of a series about setting up a Homelab using Proxmox, this is the approach I will be describing.</p>

<p>The requirements for the Snapcast server are fairly light, so a standard LXC container running Debian is more than sufficient.  I’m running it with 1 CPU and 512MB of RAM and it’s usage is averaging about 1.5% of CPU and about 128MB of RAM.  That’s on a Lenovo M910Q with an i5-6500T processor, which is not particularly high powered.</p>

<p>It’s perfectly possible that you could run this a Raspberry Pi of some sort, or anything else you have lying around.  The instructions would be largely the same, as long as you’re running Linux.</p>

<h2 id="snapcast-clients">Snapcast Clients</h2>

<p>The Snapcast client software will run on Linux, Windows, Android, IOS, and MacOS - pretty much any computing device you might have.</p>

<p>The big caveat is that to be of any use, the device needs to be able to play sound in a way that you can hear it.  This is also going to mean that the device will need to have some way to convert the digital stream into audio output.  This conversion is handled by something called a “DAC”, or “Digital to Analog Converter”.  Some devices have them built in, but may also benefit from using a dedicated DAC device of some sort.  The output from a DAC will be in a form that is compatible with headphones or an amplifier of some sort.</p>

<p>Since it runs Linux, a RaspberryPi is an excellent, low-cost, Snapcast client, and this is what I’m going to talk about in this article.  Specifically, I’m going to use RaspberryPi Zeros.</p>

<p>You can also re-purpose an old android phone or an old iPhone to be a SnapCast client.  It should probably have a headphone jack, though, as using Bluetooth to communicate with the speakers will have its own lag.  It does seem possible, however, to compensate for that in the setup of the SnapCast client.  I’m not sure how consistent the lag from Bluetooth is, though, so you’d need to experiment to see if it works.</p>

<h3 id="raspberrypi-zero">RaspberryPi Zero</h3>

<p>The RaspberryPi Zero (Pi0) is tiny, super cheap (around $20), has built-in WiFi and is easy to get.  They don’t, however, have any built-in audio output.  In order to use them for this application, you’ll need to use a external DAC.  I recommend getting one that works with the header pins on the Pi0.</p>

<p>They look like this:</p>

<p><img src="/assets/homelab/RPi0.jpg" alt="RPi0" /></p>

<p>Apparently HiFiBerry makes a great DAC designed to work with the Pi0.  Unfortunately, I wasn’t able to find any place to buy them, my usual sources have had them listed as “Out of Stock” for months, and even the HiFiBerry website doesn’t list them as available.  However, there was another brand “Pimoroni” that also make a Pi0 DAC.  So I went with that.</p>

<p>You can see the listing for the <a href="https://www.pishop.ca/product/audio-dac-shim-line-out/?searchid=0&amp;search_query=pirate+audio">Pimoroni Audio DAC SHIM</a> here on PiShop.ca.  When I checked to add the link, I see that it is currently sold out.  Even on the Pimoroni site, it’s listed as sold out.  I’m not sure if this is going to be a long-term issue.</p>

<p>This is what it looks like:</p>

<p><img src="/assets/homelab/audio-dac-shim.webp" alt="SHIM" /></p>

<p>You just shove it down on the GPIO header and it works.  No soldering.  It’s a great design.</p>

<p>If you cannot get one of these, then you’ll have to source out another DAC board.  There are others, but they are generally a bit more expensive, and a bit bigger, too.  HiFiBerry makes a variety of them that also do not require soldering.  They seem to be in stock, too. My impression is that if you use a HiFiBerry DAC, the setup instructions will be the same as for the Pimoroni SHIM.</p>

<p>Update: I received notification from PiShop.ca that the HiFiBerry DAC is now in stock.  Too late for me, but it’s a option you might want to consider.</p>

<h1 id="mopidy">Mopidy</h1>

<p>You can use any music playing software that makes sense to you.  Snapcast is capable of taking its input from a variety of pipelines, so as long as your music playing software is capable of feeding data into one of those, you’re good to go.</p>

<p>The concensus for homelabs seems to be “Modpidy”.  Mopidy can take its source from a number of different places, and there are a variety of plugins that you can install to expand this list.  This includes services like Spotify, Tidal, JellyFin, TuneIn and YouTube.  It also has an extension for SomaFM, which is what I was looking for.  Additionally, it can play local files natively.</p>

<p>The terminology in situations like this gets a little bit confusing.  We are going to talk about Mopidy installed as a <em>service</em> on the server, but it acts as a <em>client</em> to the external streaming services.</p>

<p>Additionally, Mopidy has a number of API’s available, including a web services API.  This means that create home-brewed control scripts or programs to automate aspects of Mopidy.  This also means that a number of web clients have been built and are available to install.  You’re going to need one of these if you want to control your Mopidy server in an ad-hoc fashion.</p>

<h1 id="installation-and-configuration">Installation and Configuration</h1>

<p>I spun up a Debian 13 LXC in Proxmox to be both my SnapCast and Mopidy host.  I didn’t see any reason to split these out into two containers because I view them as two sides of the same coin, and having them run in the same container makes things a bit easier.</p>

<p>As my Proxmox cluster is sitting in the basement and none of the hosts have any audio equipment attached, I couldn’t see any point in installing Mopidy first, as it would be difficult to test it.  So I chose to install SnapServer first and test it with some white noise.</p>

<h2 id="a-quick-note-about-ip-addresses-and-hostnames">A Quick Note About IP Addresses and Hostnames</h2>

<p>In my homelab I have OPNSense providing the DHCP service for the whole network, and I have a Technitium DNS server.  My usual process is to create any new servers or devices as DHCP clients.  Then I check the “leases” page in OPNSense, find the device and set it as a static address.  Then I go into the DNS server and add an entry for it with that address.  This gives me centralized control over the addressing and naming without having to reconfigure the individual servers and devices.</p>

<p>I’m not going to mention this at all in the rest of these instructions.  You’ll have to take some steps to ensure that you have all of the devices configured the way that you want them in this respect using whatever process you have adopted.</p>

<h2 id="snapcast-server-1">SnapCast Server</h2>

<p>I just created a new LXC with Debian 13 and logged into it on the console.  If you use <code class="language-plaintext highlighter-rouge">apt</code> to install <code class="language-plaintext highlighter-rouge">snapserver</code>, you’ll get version 0.31, which is a bit old.</p>

<p>At the time of writing this article, the next version of SnapCast has been released, version 0.35.0.  My instructions are going to continue to use version 0.34.0, which is about 6 months older.  Once again, you should be able to use the newer version without any other changes to the process.  I’d be careful to make sure that both the SnapCast server and the clients are all using the same version.</p>

<p>Download the package from the project on GitHub:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wget https://github.com/snapcast/snapcast/releases/download/v0.34.0/snapserver_0.34.0-1_amd64_trixie.deb
</code></pre></div></div>
<p>and then use <code class="language-plaintext highlighter-rouge">dkpg</code> to install it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># dpkg -i snapserver_0.34.0-1_amd64_trixie.deb</span>
Selecting previously unselected package snapserver.
<span class="o">(</span>Reading database ... 20235 files and directories currently installed.<span class="o">)</span>
Preparing to unpack snapserver_0.34.0-1_amd64_trixie.deb ...
Unpacking snapserver <span class="o">(</span>0.34.0-1<span class="o">)</span> ...
Setting up snapserver <span class="o">(</span>0.34.0-1<span class="o">)</span> ...
Installing new version of config file /etc/default/snapserver ...
Installing new version of config file /etc/init.d/snapserver ...
Installing new version of config file /etc/snapserver.conf ...
Processing triggers <span class="k">for </span>man-db <span class="o">(</span>2.13.1-1<span class="o">)</span> ...
</code></pre></div></div>

<p>This handles the setup as a service.  You can check it right away:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># systemctl status snapserver -n40</span>
<span class="k">*</span> snapserver.service - Snapcast server
     Loaded: loaded <span class="o">(</span>/usr/lib/systemd/system/snapserver.service<span class="p">;</span> enabled<span class="p">;</span> preset: enabled<span class="o">)</span>
     Active: active <span class="o">(</span>running<span class="o">)</span> since Mon 2026-04-06 17:28:09 UTC<span class="p">;</span> 26s ago
 Invocation: 6c949040b6dc401485092210bfce4ce5
       Docs: man:snapserver<span class="o">(</span>1<span class="o">)</span>
   Main PID: 1386 <span class="o">(</span>snapserver<span class="o">)</span>
      Tasks: 3 <span class="o">(</span>limit: 18886<span class="o">)</span>
     Memory: 2.2M <span class="o">(</span>peak: 2.5M<span class="o">)</span>
        CPU: 83ms
     CGroup: /system.slice/snapserver.service
             <span class="sb">`</span><span class="nt">-1386</span> /usr/bin/snapserver <span class="nt">--logging</span>.sink<span class="o">=</span>system <span class="nt">--server</span>.datadir<span class="o">=</span>/var/lib/snapserver

Apr 06 17:28:09 testsnap systemd[1]: Started snapserver.service - Snapcast server.
Apr 06 17:28:09 testsnap snapserver[1386]: <span class="o">(</span>Snapserver<span class="o">)</span> Version 0.34.0, revision 0ac55084
Apr 06 17:28:09 testsnap snapserver[1386]: <span class="o">(</span>Snapserver<span class="o">)</span> Stream plugin directory: <span class="s1">'"/usr/share/snapserver/plug-ins"'</span>, sandbox directory: <span class="s1">'"/usr/share/snapserver/&gt;
Apr 06 17:28:09 testsnap snapserver[1386]: (Snapserver) Adding source: pipe:///tmp/snapfifo?name=default
Apr 06 17:28:09 testsnap snapserver[1386]: (Avahi) Adding service '</span>Snapcast<span class="s1">'
Apr 06 17:28:09 testsnap snapserver[1386]: (Snapserver) Using HTTP host name: testsnap
Apr 06 17:28:09 testsnap snapserver[1386]: (PcmStream) PcmStream: default, sampleFormat: 48000:16:2
Apr 06 17:28:09 testsnap snapserver[1386]: (PipeStream) PipeStream mode: create
Apr 06 17:28:09 testsnap snapserver[1386]: (Server) Stream: {"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"defa&gt;
Apr 06 17:28:09 testsnap snapserver[1386]: (FlacEnc) Init - compression level: 2
Apr 06 17:28:09 testsnap snapserver[1386]: (ControlServer) Creating TCP control acceptor for address: ::, port: 1705
Apr 06 17:28:09 testsnap snapserver[1386]: (ControlServer) Creating HTTP acceptor for address: ::, port: 1780
Apr 06 17:28:09 testsnap snapserver[1386]: (StreamServer) Creating TCP stream acceptor for address: ::, port: 1704
Apr 06 17:28:09 testsnap snapserver[1386]: (Snapserver) Number of threads: 2, hw threads: 1
Apr 06 17:28:09 testsnap snapserver[1386]: (AsioStream) Error reading message: End of file, length: 0, ec: asio.misc:2
Apr 06 17:28:10 testsnap snapserver[1386]: (Avahi) Service '</span>Snapcast<span class="s1">' successfully established.
</span></code></pre></div></div>
<p>It’s even enabled! However, it has failed to start. Let’s see why.  Look at this line of the output:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Apr 06 17:28:09 testsnap snapserver[1386]: <span class="o">(</span>Snapserver<span class="o">)</span> Adding <span class="nb">source</span>: pipe:///tmp/snapfifo?name<span class="o">=</span>default
</code></pre></div></div>
<p>See how it says “…/tmp/snapfifo…”?  That’s going to cause problems.</p>

<p>There’s a new security feature in Debian that locks down fifo files in “sticky” directories like <code class="language-plaintext highlighter-rouge">/tmp</code> in order to make spoofing attempts harder.  You can disable this by turning off the <code class="language-plaintext highlighter-rouge">fs.protected_fifos</code> kernel parameter, but this is probably not a good idea.  Instead, move the Snapcast input fifo to somewhere else.</p>

<p>To do this, you’ll need to edit the <code class="language-plaintext highlighter-rouge">/etc/snapserver.conf</code> file.  It’s big, and has lots of stuff in it, but the section you want to look at is called “[stream]”:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Stream settings #############################################################</span>
<span class="c">#</span>
<span class="o">[</span>stream]
<span class="c"># address to listen on, can be specified multiple times</span>
<span class="c"># use "0.0.0.0" to bind to any IPv4 address or :: to bind to any IPv6 address</span>
<span class="c"># or "127.0.0.1" or "::1" to bind to localhost IPv4 or IPv6, respectively</span>
<span class="c"># use the address of a specific network interface to just listen to and accept</span>
<span class="c"># connections from that interface</span>
<span class="c">#bind_to_address = ::</span>

<span class="c"># which port the server should listen to</span>
<span class="c">#port = 1704</span>

<span class="c"># source URI of the PCM input stream, can be configured multiple times</span>
<span class="c"># The following notation is used in this paragraph:</span>
<span class="c">#  &lt;angle brackets&gt;: the whole expression must be replaced with your specific setting</span>
<span class="c"># [square brackets]: the whole expression is optional and can be left out</span>
<span class="c"># [key=value]: if you leave this option out, "value" will be the default for "key"</span>
<span class="c">#</span>
<span class="c"># Format: TYPE://host/path?name=&lt;name&gt;[&amp;codec=&lt;codec&gt;][&amp;sampleformat=&lt;sampleformat&gt;][&amp;chunk_ms=&lt;chunk ms&gt;][&amp;controlscript=&lt;control script filename&gt;[&amp;controlscriptparams=&lt;control script command line arguments&gt;]]</span>
<span class="c">#  parameters have the form "key=value", they are concatenated with an "&amp;" character</span>
<span class="c">#  parameter "name" is mandatory for all sources, while codec, sampleformat and chunk_ms are optional</span>
<span class="c">#  and will override the default codec, sampleformat or chunk_ms settings</span>
<span class="c"># Available types are:</span>
<span class="c"># pipe: pipe:///&lt;path/to/pipe&gt;?name=&lt;name&gt;[&amp;mode=create], mode can be "create" or "read"</span>
<span class="c"># librespot: librespot:///&lt;path/to/librespot&gt;?name=&lt;name&gt;[&amp;username=&lt;my username&gt;&amp;password=&lt;my password&gt;][&amp;devicename=Snapcast][&amp;bitrate=320][&amp;wd_timeout=7800][&amp;volume=100][&amp;onevent=""][&amp;nomalize=false][&amp;autoplay=false][&amp;params=&lt;generic librepsot process arguments&gt;]</span>
<span class="c">#  note that you need to have the librespot binary on your machine</span>
<span class="c">#  sampleformat will be set to "44100:16:2"</span>
<span class="c"># file: file:///&lt;path/to/PCM/file&gt;?name=&lt;name&gt;</span>
<span class="c"># process: process:///&lt;path/to/process&gt;?name=&lt;name&gt;[&amp;wd_timeout=0][&amp;log_stderr=false][&amp;params=&lt;process arguments&gt;]</span>
<span class="c"># airplay: airplay:///&lt;path/to/airplay&gt;?name=&lt;name&gt;[&amp;port=5000]</span>
<span class="c">#  note that you need to have the airplay binary on your machine</span>
<span class="c">#  sampleformat will be set to "44100:16:2"</span>
<span class="c"># tcp server: tcp://&lt;listen IP, e.g. 127.0.0.1&gt;:&lt;port&gt;?name=&lt;name&gt;[&amp;mode=server]</span>
<span class="c"># tcp client: tcp://&lt;server IP, e.g. 127.0.0.1&gt;:&lt;port&gt;?name=&lt;name&gt;&amp;mode=client</span>
<span class="c"># alsa: alsa:///?name=&lt;name&gt;&amp;device=&lt;alsa device&gt;[&amp;send_silence=false][&amp;idle_threshold=100][&amp;silence_threshold_percent=0.0]</span>
<span class="c"># meta: meta:///&lt;name of source#1&gt;/&lt;name of source#2&gt;/.../&lt;name of source#N&gt;?name=&lt;name&gt;</span>
<span class="nb">source</span> <span class="o">=</span> pipe:///tmp/snapfifo?name<span class="o">=</span>default
</code></pre></div></div>
<p>There’s more in the “[stream]” section, but I stopped at the line we need to change, “source”.</p>

<p>Change it to this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">source</span> <span class="o">=</span> pipe:///var/local/snapserver/snapfifo?name<span class="o">=</span>default
</code></pre></div></div>
<p>You’ll have to create <code class="language-plaintext highlighter-rouge">/var/local/snapserver</code> directory, and set the ownership:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># mkdir /var/local/snapserver</span>
<span class="c"># chown snapserver /var/local/snapserver</span>
<span class="c"># chgrp snapserver /var/local/snapserver</span>
<span class="c"># systemctl start snapserver</span>
<span class="c"># systemctl status snapserver -n40</span>
<span class="k">*</span> snapserver.service - Snapcast server
     Loaded: loaded <span class="o">(</span>/usr/lib/systemd/system/snapserver.service<span class="p">;</span> enabled<span class="p">;</span> preset: enabled<span class="o">)</span>
     Active: active <span class="o">(</span>running<span class="o">)</span> since Mon 2026-04-06 17:36:01 UTC<span class="p">;</span> 40s ago
 Invocation: 022347aa459a41d0b8edb82e4c5eeaf5
       Docs: man:snapserver<span class="o">(</span>1<span class="o">)</span>
   Main PID: 1446 <span class="o">(</span>snapserver<span class="o">)</span>
      Tasks: 3 <span class="o">(</span>limit: 18886<span class="o">)</span>
     Memory: 2.4M <span class="o">(</span>peak: 2.4M<span class="o">)</span>
        CPU: 115ms
     CGroup: /system.slice/snapserver.service
             <span class="sb">`</span><span class="nt">-1446</span> /usr/bin/snapserver <span class="nt">--logging</span>.sink<span class="o">=</span>system <span class="nt">--server</span>.datadir<span class="o">=</span>/var/lib/snapserver

Apr 06 17:36:01 testsnap systemd[1]: Started snapserver.service - Snapcast server.
Apr 06 17:36:01 testsnap snapserver[1446]: <span class="o">(</span>Snapserver<span class="o">)</span> Version 0.34.0, revision 0ac55084
Apr 06 17:36:01 testsnap snapserver[1446]: <span class="o">(</span>Snapserver<span class="o">)</span> Stream plugin directory: <span class="s1">'"/usr/share/snapserver/plug-ins"'</span>, sandbox directory: <span class="s1">'"/usr/share/snapserver/&gt;
Apr 06 17:36:01 testsnap snapserver[1446]: (Snapserver) Adding source: pipe:///var/local/snapserver/snapfifo?name=default
Apr 06 17:36:01 testsnap snapserver[1446]: (Avahi) Adding service '</span>Snapcast<span class="s1">'
Apr 06 17:36:01 testsnap snapserver[1446]: (Snapserver) Using HTTP host name: testsnap
Apr 06 17:36:01 testsnap snapserver[1446]: (PcmStream) PcmStream: default, sampleFormat: 48000:16:2
Apr 06 17:36:01 testsnap snapserver[1446]: (PipeStream) PipeStream mode: create
Apr 06 17:36:01 testsnap snapserver[1446]: (Server) Stream: {"fragment":"","host":"","path":"/var/local/snapserver/snapfifo","query":{"chunk_ms":"20","codec":"f&gt;
Apr 06 17:36:01 testsnap snapserver[1446]: (FlacEnc) Init - compression level: 2
Apr 06 17:36:01 testsnap snapserver[1446]: (ControlServer) Creating TCP control acceptor for address: ::, port: 1705
Apr 06 17:36:01 testsnap snapserver[1446]: (ControlServer) Creating HTTP acceptor for address: ::, port: 1780
Apr 06 17:36:01 testsnap snapserver[1446]: (StreamServer) Creating TCP stream acceptor for address: ::, port: 1704
Apr 06 17:36:01 testsnap snapserver[1446]: (Snapserver) Number of threads: 2, hw threads: 1
Apr 06 17:36:01 testsnap snapserver[1446]: (AsioStream) Error reading message: End of file, length: 0, ec: asio.misc:2
Apr 06 17:36:02 testsnap snapserver[1446]: (Avahi) Service '</span>Snapcast<span class="s1">' successfully established.  
</span></code></pre></div></div>

<p>Some online discussions about this have suggested <code class="language-plaintext highlighter-rouge">/var/run/snapserver</code>, but I have found that <code class="language-plaintext highlighter-rouge">/var/run</code> is cleaned up automatically during system boot and <code class="language-plaintext highlighter-rouge">/var/run/snapserver</code> gets deleted.  You can automate the creation in the service startup, but <code class="language-plaintext highlighter-rouge">/var/local/snapserver</code> seems just fine to me.</p>

<h2 id="testing">Testing</h2>

<p>In order to test your SnapCast server, you’ll need some clients.  The easiest one comes with the server, and it’s available in a web page.</p>

<h3 id="snapweb">Snapweb</h3>

<p>Open up a browser and go to port <code class="language-plaintext highlighter-rouge">1780</code> on whatever the address or hostname of your SnapCast server is.  You should get a mostly black page with “Snapcast” and a hamburger menu at the top left, and a “Play” button on the top right.  Go ahead and click on that “Play” button and you should get this:</p>

<p><img src="/assets/homelab/SnapWeb.png" alt="ScreenSnap" /></p>

<p>At this point, you would be hearing something if there was anything streaming.  Let’s pump some white noise into that fifo file back on the server:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># cat /dev/urandom &gt; /var/local/snapserver/snapfifo</span>
</code></pre></div></div>
<p>Now you should hear something.  If you do, then you have now confirmed that the server setup is correct, and if you have problems going forward, it’s probably not going to be the server, but your client setup.</p>

<h1 id="conclusion">Conclusion</h1>

<p>This wraps up Part I.</p>

<p>At this point, you should have a basic understanding about how SnapCast works, and, if you’ve followed along, you’ll have a working SnapCast server.  It’s not a lot of use yet, but we’ll get closer to that in Part II of this series.</p>

<p>Part II continues with the Raspberry Pi SnapCast clients.</p>]]></content><author><name>Dave Barrett</name><email>pragmaticcoding8@gmail.com</email></author><category term="homelab" /><summary type="html"><![CDATA[A way to stream audio around your entire house using inexpensive RaspberryPi computers and SnapCast]]></summary></entry><entry><title type="html">VLAN’s and Subnets For Home Networks</title><link href="https://www.pragmaticcoding.ca/homelab/vlans" rel="alternate" type="text/html" title="VLAN’s and Subnets For Home Networks" /><published>2026-02-06T17:00:00+00:00</published><updated>2026-02-06T17:00:00+00:00</updated><id>https://www.pragmaticcoding.ca/homelab/VLANs</id><content type="html" xml:base="https://www.pragmaticcoding.ca/homelab/vlans"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>Back in 2003 I was doing some work in the company’s accounting department when all of the PC’s in the area flicked off and rebooted again.  It was so fast and simultaneous that I thought that we’d had some kind of electrical fault that hit that part of the office, and I didn’t think too much about it.  Comparing notes with one of the other IT guys much later on, he had also noticed the same thing in another part of the office.</p>

<p>Sometime later, our Internet connection was down.  Or so I thought.</p>

<p>I got in touch with our ISP and they checked and told me that, no, the connection was up and there was actually a LOT of traffic going through it.  So I went into the computer room to check.</p>

<p>Back in those days, our connection between the our firewall and the T3 modem was through an old 10Base-T hub.  Not a switch, but a hub.  This was okay, because these were the only two devices on the hub, and the 10Mbps, was still faster than our Internet connection.</p>

<p>The hub had a little red light on it that flashed whenever it detected a collision when two nodes try to transmit at exactly the same time and interfered with each other. During normal operations, you’d expect that light to flicker once or twice a minute.  Any more, and you likely had a problem brewing.</p>

<p>Well, I went into the computer room and that collision light was solid.</p>

<p>Not flickering, just on.</p>

<p>There was so much traffic going down that hub that it was jammed up into constant collisions and nothing was getting through.  Back in those days, <strong>we hosted our own web sites on premises, and this meant that we had essentially been kicked off the web</strong>.</p>

<p>Our firewall logs had gotten so big in that short time that it had run out of disk space and stopped logging.  I had to flush the logs and restart it just to find out what that traffic was.</p>

<p>It turns out that we had been hit with a worm.  Probably the “Welchia”, or the “Blaster” worm.  One of the characteristics of it was that, after installing itself, it caused the system to reboot.  After that, each infected computer would start scanning like crazy to find more Windows systems that it could infect.</p>

<p>It was those scanning probe packets that were jamming up our internet connection.  That simultaneous reboot that I had seen was actually a handful of computers getting infected at nearly exactly the same time.</p>

<p>We lost of few days of our lives to investigating this, working around it, and cleaning and patching the workstations.</p>

<p>At some point in our investigations, we determined that the virus was spread from a laptop that one of our sales/marketing guys was using.  He had taken it home or to a client site, and plugged it into a network and got infected.  Then he came into the office and plugged it in - <strong>behind our firewall</strong>.</p>

<p>It was like the babysitter late at night in the horror movie.  The phone call is coming from the basement - inside the house!</p>

<p>We had no defence against this.</p>

<h2 id="smart-home-devices">Smart Home Devices</h2>

<p>None of us have no problem going to the electronics store or the hardware store or the appliance store and coming back with some new “smart” device.  A fridge.  A robot vaccuum. Even a smart plug.</p>

<p>And when we get it home, we take it out of the box, connect to it from our phones and give it our wifi password.</p>

<p>Now it’s in our networks.  Behind the firewall…inside the house!</p>

<p>Yeah.  But what can a smart plug do?</p>

<p>Indeed.  What if it opens a VPN tunnel back to some hacker den in North Korea?  Really, I have no doubt that you could stuff enough brains inside a smart plug to do that.  And once that’s done, the hackers half-way around the world can start poking around inside your entire home network.  And you would probably never know.</p>

<p>I’m seeing articles on-line that some refrigerators with display screens on them are starting to show ads.  What if you don’t want ads on your fridge?  What if I don’t want my printer reporting back to HP about how many pages I print each month? And let’s not talk about “Smart” TVs.</p>

<p>I did a survey on my network, and between smart plugs and switches, robot vaccuums, garage door openers, streaming devices, smart speakers, printers and a few other things, I counted up about 45 devices.</p>

<p>What do I know about any of them?</p>

<p>Honestly, not much.  So why trust them?</p>

<h2 id="zero-trust">Zero Trust</h2>

<p>When it comes to this stuff, “trust” is a very misguided concept.</p>

<p>Do you need to trust these things?  The answer is, “No!”</p>

<p>What you need is a “Zero Trust” network design behind your firewall that lets you sequester these devices such that they can do no harm.  Then you don’t care if they are malicious, because they’ve been neutralized.</p>

<p>Let’s look at how to do that…</p>

<h1 id="tcpip-subnets-and-routing">TCP/IP Subnets and Routing</h1>

<p>A quick note to start: This article is about IPv4 routing, not IPv6.  So if you are using IPv6, then you won’t need the information here.</p>

<p>Before we can go any further, we’ll need to have a basic understanding about how devices find each other and communicate over TCP/IP.</p>

<p>If you’ve ever manually set up a device on a network without DHCP, you’ll know that you need to provide four pieces of information:</p>

<dl>
  <dt>The IP Address</dt>
  <dd>
    <p>This is the literally the address of the computer on the network.  It’s going to be in the format of <code class="language-plaintext highlighter-rouge">###.###.###.###</code> where each <code class="language-plaintext highlighter-rouge">###</code> is a number between 1-255 (ie. one byte). An IP address is always 4 bytes long.</p>
  </dd>
  <dt>The NetMask</dt>
  <dd>
    <p>This tells the device the address space of the subnet on which it sits.  It will use this information to determine if a device that it wants to communicate with is on its own subnet or not.  We’re going to use a netmask of <code class="language-plaintext highlighter-rouge">255.255.255.0</code> or <code class="language-plaintext highlighter-rouge">/24</code> in CIDR notation for all the subnets in this article which means that the first 3 bytes of the address (or the first 24 bits of the address) represent the subnet while the last byte (8 bits) represents the device’s unique place in that subnet.</p>
  </dd>
  <dt>The Default Gateway</dt>
  <dd>
    <p>This should be considered to be a feature of the subnet, rather than the device that you are configuring (for purposes of this discussion).  It tells the device the IP address of where to send its packets if it wants to communicate with a device that it has determined is NOT on its own subnet.</p>
  </dd>
  <dt>The DNS Server Addresses</dt>
  <dd>
    <p>This is where to send requests for DNS lookups.  It’s not relevant to this discussion.</p>
  </dd>
</dl>

<h2 id="how-devices-determine-routing">How Devices Determine Routing</h2>

<p>For purposes of this discussion, it’s probably best if you consider TCP/IP to be a “broadcast” protocol.  This means, especially from the viewpoint of each individual device, that any packets it sends out are distributed throughout the subnet with an address attached to them.  Only the device that has that address will pay any attention to it.  In reality, modern switches and networking equipment handle the packet delivery without the need to actually send every packet to every device - but the devices don’t see that.</p>

<p>Each device knows its own IP address and its netmask so that it can determine if any other device with which it wants to communicate has an address on the same subnet as itself.  If so, then it just broadcasts its packets on its subnet with the destination IP address on it.  The other device will see its own address and get the packet.</p>

<p>If it determines that the other device is NOT on the same subnet, then it still broadcasts it on its own subnet, but this time addressed to the subnet’s default gateway - with the actual address of the destination somehow encapsulated inside it.  The gateway will get the packet, and then relay it off in some manner so that it gets to its destination.</p>

<p>Here’s an example of what that would look like:</p>

<p><img src="/assets/homelab/SubnetAndGateway.png" alt="Subnet Diagram" /></p>

<p>In this diagram, the top two PC’s belong to the <code class="language-plaintext highlighter-rouge">192.168.1.0</code> network, which is addresses <code class="language-plaintext highlighter-rouge">192.168.1.0</code> to <code class="language-plaintext highlighter-rouge">192.168.1.255</code>. This is because they have the netmask <code class="language-plaintext highlighter-rouge">255.255.255.0</code> which you can see from the CIDR notation <code class="language-plaintext highlighter-rouge">/24</code> in the addresses.  These two computers can talk to each other directly.</p>

<p>If they want to talk to <code class="language-plaintext highlighter-rouge">Server1</code>, with address <code class="language-plaintext highlighter-rouge">10.196.212.116</code>, then they will determine that it is NOT on their own subnet, so they have to send their packets to their default gateway, which is <code class="language-plaintext highlighter-rouge">2811 Router</code> with the address <code class="language-plaintext highlighter-rouge">192.168.1.1</code>.  That router will then send it on to <code class="language-plaintext highlighter-rouge">10.196.212.116</code> through its second network interface, which has the address <code class="language-plaintext highlighter-rouge">10.196.212.1</code>.</p>

<p>What’s very important to understand here is that all three PC’s are connected to the same switch!  Even so, only PC1 and PC2 can talk to each other directly, and any other communication requires relaying through the Router.</p>

<p>This is because the physical topology of the network is irrelevant to the TCP/IP routing.</p>

<h2 id="routing-with-firewalls">Routing With Firewalls</h2>

<p>For this discussion, your really big takeaway should be this…</p>

<p>The router in the diagram with the name <code class="language-plaintext highlighter-rouge">2811 Router</code> <em>could</em> be a firewall.  As a firewall, it would have rules that control if and how devices communicate between subnets.</p>

<p>As a matter of fact, firewalls can <strong>only</strong> control traffic between subnets and not between nodes on the same subnet.  For instance, in the diagram above, there is no way that the firewall at <code class="language-plaintext highlighter-rouge">2811 Router</code> could control traffic between <code class="language-plaintext highlighter-rouge">PC1</code> and <code class="language-plaintext highlighter-rouge">PC2</code> because they can communicate directly without involving the router/firewall.  On the other hand, rules in the firewall at <code class="language-plaintext highlighter-rouge">2811 Router</code> will control communication between <code class="language-plaintext highlighter-rouge">Server 1</code> and either of <code class="language-plaintext highlighter-rouge">PC1</code> or <code class="language-plaintext highlighter-rouge">PC2</code>.</p>

<p>From this, you can deduce that the IP subnet is the basic building block of home network security.</p>

<h1 id="introducing-vlans">Introducing VLAN’s</h1>

<p>If you do some web searches about VLAN’s, you’ll find something like this introduction from Wikipedia:</p>

<blockquote>
  <p>A virtual local area network (VLAN) is a local area network broadcast domain that is partitioned and isolated in a virtual network at the data link layer (OSI layer 2). A VLAN behaves like a virtual network switch or network link that can share the same physical structure with other VLANs while staying logically separate from them.</p>

  <p>VLANs work by applying tags to network frames that are forwarded within the broadcast domain, creating the appearance and functionality of network traffic that behaves as if it were split between separate networks. In this way, VLANs can keep network applications separate despite being connected to the same physical network, and without requiring multiple sets of cabling and networking devices to be deployed.</p>
</blockquote>

<p>Essentially this is describing something that looks like this:</p>

<p><img src="/assets/homelab/VLAN1.png" alt="VLAN Diagram" /></p>

<p>You can see that although all of the ports are on the same switch, ports 1-4 cannot communicate with ports 5-8.  Essentially, this turns the single switch into two switches.</p>

<p>You can also split a VLAN across switches, like this:</p>

<p><img src="/assets/homelab/VLAN2.webp" alt="VLAN With 2 Switches" /></p>

<p>This divides both switches into 3 parts, and the ports in both switches can communicate with all of the ports of the same colour across both switches.</p>

<p>And that’s about all you’ll get before you’re really deep, deep into the weeds.  You’ll quickly find out about trunks and IEEE 802.1Q and lots of other complicated stuff.</p>

<p>This seems like a cool idea…until you start wondering about how something on one VLAN can communicate with something on different VLAN.</p>

<p>After all, if you can’t do that, then your VLAN is 100% sequestered and inaccessible to and from the rest of the world.  And that is quite a bit less than useful.</p>

<h2 id="managed-switches">Managed Switches</h2>

<p>Before we get into the details, we need to talk about one more thing…</p>

<p>Your standard, consumer grade network switch that you can buy for about $10 on any street corner probably won’t be able to handle VLAN’s. You’ll need something called a “Managed Switch”, which is generally marketted at corporate customers.  They’re a bit more expensive than unmanaged switches, but you should be able to pick them up for $30 or less.  You will, however, have to ditch any unmanaged switches that you already have - you could find a way to use them, but it’s not worth the hassle.</p>

<p>A managed switch will generally allow you to do a whole bunch of things in addition to VLAN’s, things like “spanning trees” and trunking.  We’re not going to talk about that stuff here, though.  Just VLAN’s.</p>

<h2 id="kinds-of-port-membership-in-vlans">Kinds of Port Membership in VLAN’s</h2>

<p>If you read the Wikipedia snippet from above, you’ll have noticed that VLAN’s work by attaching “tags” (whatever they are) to network packets before they are broadcast around the network.  The “tag” is just an numeric ID - generally between 2 and 1000 - and switch ports that understand VLAN’s will be programmed to only transmit and receive packets with particular tags.</p>

<p>Tag <code class="language-plaintext highlighter-rouge">1</code> is special.  It’s the default “no VLAN” tag, and get’s slapped on any untagged packet that travels around VLAN aware network equipment that hasn’t been configured to do anything different.  You cannot use tag <code class="language-plaintext highlighter-rouge">1</code> for anything else.</p>

<p>From my experience, when you configure a managed switch to use VLAN’s you need to define each VLAN tag within the switch, and then configure how each port interacts with each VLAN that you have defined.  Let’s look at how this works…</p>

<p>Ports on managed switches can handle VLAN’s in two ways:</p>

<dl>
  <dt>Untagged Packets</dt>
  <dd>
    <p>This is what you see in most of the introductory articles and descriptions.  In this case, the port is configured to add a VLAN tag to any packets that come into it that aren’t already tagged.  This means that if you have some device that is not VLAN aware, the port on the switch will assign the device to a VLAN for it.  The default setting for this is almost always to add VLAN <code class="language-plaintext highlighter-rouge">1</code>.</p>
  </dd>
  <dt>Tagged Packets</dt>
  <dd>
    <p>This is a mode where the switch port is assigned membership of one <strong>or more</strong> VLAN’s and it will allow incoming packets tagged with those VLAN id’s to pass through.  Packets tagged with any other VLAN’s will be blocked.  This means that if you plug a VLAN aware device into the port, that device can decide which VLAN’s it wants to communicate on, assuming the port is a member of those VLAN’s.</p>
  </dd>
</dl>

<h3 id="configuring-the-switch">Configuring the Switch</h3>

<p>Obviously, the interface for configuring each brand of switch is going to be different.  Generally, though, you’ll be looking for a section with a name like “802.1Q VLAN”.</p>

<p>In my D-Link switch the interface looks like this:</p>

<p><img src="/assets/homelab/VLAN5.png" alt="D-Link VLAN" /></p>

<p>If you click on one of the VLAN number links, you get a screen like this:</p>

<p><img src="/assets/homelab/VLAN6.png" alt="D-Link VLAN" /></p>

<p>The method with this switch is that you define each VLAN, give it a name and then define how each port interacts with the VLAN.</p>

<p>In this switch, the default VLAN <code class="language-plaintext highlighter-rouge">1</code> remains the VLAN to which all untagged packets are attached.  Then all of the ports are configured as tagged members of all of the custom VLAN’s that I created.  Essentially, every port will be able to communicate on every VLAN if the packets are already tagged, and will leave the untagged packets untagged.</p>

<p>I should point out that this is <strong>not</strong> the final configuration that I intend for this switch.  I’m still in the process of sorting out all of my network security and I’m leaving this wide open while I configure everything else and test it.  I don’t want something to fail and waste lots of time looking for setup issues in my firewall whilst the actual problem was the wrong VLAN on a port on this switch.  This switch is the one closest to my entertainment centre, and has most of the “VLAN unaware” wired devices on my network.  Things like my XBox and my IPTV box.  Eventually, I’ll be reconfiguring this switch such that those devices go into the IOT VLAN (I think).</p>

<h2 id="understanding-tagged-vlan-membership">Understanding Tagged VLAN Membership</h2>

<p>You might find it a little hard to picture how this second case is used at first.</p>

<p>However, if you are using Proxmox for virtualization, you’ll have seen this screen for configuring the networking on a Proxmox host:</p>

<p><img src="/assets/homelab/VLAN3.png" alt="Screen Snap" /></p>

<p>Here the Linux Bridge <code class="language-plaintext highlighter-rouge">vmbr0</code>, which defines how the VM’s on the node will connect to the ethernet adapter, is defined.  Note the checkbox beside “VLAN aware:”, and the range of “2-4094” that is assigned to “VLAN IDs:”.</p>

<p>Then, when you define the networking for a VM on that Proxmox host, you’ll see this screen:</p>

<p><img src="/assets/homelab/VLAN4.png" alt="Screen Snap" /></p>

<p>In this screen <code class="language-plaintext highlighter-rouge">eth0</code> is being defined as connecting to <code class="language-plaintext highlighter-rouge">vmbr0</code> and having VLAN tag <code class="language-plaintext highlighter-rouge">20</code> attached.</p>

<p>What’s important to note is that if we define another Linux container or VM on this same node, attached to the same bridge, but assigned to VLAN tag <code class="language-plaintext highlighter-rouge">30</code>, then it won’t be able to communicate directly with the first VM.  Even though they are sharing the exact same hardware.</p>

<p>However, these VM’s will <strong>not</strong> be able to communicate with anything unless the port on the switch that the host is plugged into is a member of both VLAN’s <code class="language-plaintext highlighter-rouge">20</code> and <code class="language-plaintext highlighter-rouge">30</code> as “tagged”.</p>

<h2 id="firewalls-and-routers">Firewalls and Routers</h2>

<p>The second, and probably most important place you’ll encounter a “VLAN aware” device is a firewall or a router.  For sure, both OPNsense and pfSense are very VLAN aware.</p>

<p>Understand that a firewall is always, by definition, a router, and that a router is designed to transfer traffic between ports.  Essentially, firewalls use VLAN’s to define multiple network interfaces on the same hardware port.  Each VLAN becomes a “device” which can have its own ethernet address.</p>

<p>In OPNsense, you define a VLAN as a device, and attach it to a physical ethernet port and connect it logically to a VLAN defined on the switch.  From that point on, you can treat your VLAN device just like any other network port.  You can assign it an IP address, you can create firewall rules for it, and you can create a DHCP server for it.</p>

<p>This is what enables you to use VLAN’s to control security inside your network.  Once you’ve done this, you’ve created the authoritative connection between a VLAN and a TCP/IP subnet.</p>

<p>Let’s say that you have that VLAN <code class="language-plaintext highlighter-rouge">20</code> from the screenshot above.  On your firewall, you define a network interface called <code class="language-plaintext highlighter-rouge">VLAN_DMZ</code> and assign it to your inside ethernet port using VLAN <code class="language-plaintext highlighter-rouge">20</code>.  You’re going to use <code class="language-plaintext highlighter-rouge">VLAN_DMZ</code> as the gateway for the network <code class="language-plaintext highlighter-rouge">10.10.10.0/24</code>, so you give it the address <code class="language-plaintext highlighter-rouge">10.10.10.1</code>.</p>

<p>Now, if any other device inside your network is on VLAN <code class="language-plaintext highlighter-rouge">20</code>, and if it wants to talk to anything outside VLAN <code class="language-plaintext highlighter-rouge">20</code> it is going to have to be on <code class="language-plaintext highlighter-rouge">10.10.10.0/24</code>.  Otherwise it cannot communicate with the gateway at <code class="language-plaintext highlighter-rouge">10.10.10.1</code>.</p>

<p>You can see from this that the firewall configuration “locks in” VLAN <code class="language-plaintext highlighter-rouge">20</code> to the <code class="language-plaintext highlighter-rouge">10.10.10.0/24</code> subnet, and the firewall rules will define how it can behave as a “DMZ”.</p>

<p>So far, none of this particularly uses the isolation qualities of VLAN’s.  It really just enables us to have just about as many subnets as we want on our network without having to stuff a new NIC inside our firewall for each subnet.</p>

<p>In fact, you could just ignore the security aspects of the separation qualities of VLAN’s and configure every single port on every single switch that you own to accept traffic from every single VLAN that you create and you probably wouldn’t sacrifice too much actual security.  I’m not suggesting that you do this, but it wouldn’t be the end of the world if you did.</p>

<h2 id="dhcp">DHCP</h2>

<p>The one place where it really is useful to have separation between the VLAN’s is the one commonly used TCP/IP service which is truly treated as a broadcast - DHCP.</p>

<p>When a device is attached to the network that has been configured to use DHCP to get its network information it doesn’t initially have an IP address or even any knowledge about the network that it’s plugging into.  How does it communicate with the DHCP server on the network without an IP address?</p>

<p>The answer is that it uses pure local broadcast on the IP network.  Essentially, all of the communication is performed by using the address <code class="language-plaintext highlighter-rouge">255.255.255.255</code> which will be seen by every host on the local network.  Most importantly, packets addressed to <code class="language-plaintext highlighter-rouge">255.255.255.255</code> will never pass through a router - they are strictly local.</p>

<p>First, the device broadcasts a “discover” message to the entire network that essentially says, “I’m here! Can anybody give me an address?”  Every DHCP server that sees that broadcast will respond by broadcasting an “offer” of an IP address.  The new device then picks one of the offers, and “requests” that address be assigned to it (once again by broadcast).  The DHCP server that made the offer then broadcasts an “acknowledgement” of the assignment.</p>

<p>This process is known as “DORA”, which means Discover-Offer-Request-Acknowledge.</p>

<p>The key point for us is the part that says, “Every DHCP server that sees that broadcast…”.  Now go back and look at the first VLAN picture in this article.  See how it says, “Broadcast message in”, and “Broadcast message out”? And see how the VLAN’s prevent the broadcasts from crossing between them?</p>

<p>What this means is that every single VLAN on your network needs its own DHCP server if you want to use DHCP.  In our example, we would create a DHCP service for <code class="language-plaintext highlighter-rouge">VLAN_DMZ</code>, which listens and broadcasts on VLAN 20.</p>

<p>Once again OPNSense works great here.  Using “Dnsmasq DNS and DHCP” - which is the DHCP service that you should be using now - you don’t set up multiple DHCP servers, but you do create up rule sets for different interfaces.  OPNSense will then listen on each interface that you’ve configured, and provide an address, mask, gateway and DNS server tailored for that VLAN.</p>

<p>This is the piece that puts it all together.</p>

<p>Once you’ve done this, you can now connect a device to your network configured to use DHCP and assigned to a VLAN - either through the device itself or through the switch that it’s plugged into - and it will be attached to a particular subnet that you have defined for that VLAN.</p>

<h2 id="wifi">WiFi</h2>

<p>But what about all those devices that connect via WiFi?  They’re generally not VLAN aware, and they don’t plug into a switch port that you can configure for untagged packets.</p>

<p>The answer is that most WiFi access points allow you to associate a WiFi SSID with a VLAN.  Even consumer grade WiFi routers allow you to set up a “Guest” SSID and associate it with a VLAN.</p>

<p>I’ve been using D-Link DBR-X3000-AP access points, and they have the ability to set up 6 “Guest” zones, each with an SSID and VLAN tag:</p>

<p><img src="/assets/homelab/GuestZone.png" alt="WiFi VLAN" /></p>

<p>Now, when I’m setting up a new smart plug or other device, I just configure it to connect to my “IOT” SSID, and to use DHCP.  Everything else just works.  You’ll also have to make sure that whatever switch port you plug the AP into is configured to recognize the VLAN’s that you use for all of the SSID’s that you create.</p>

<h1 id="the-default-setup-with-isp-equipment">The Default Setup with ISP Equipment</h1>

<p>Chances are that you started out with, and might still have, the WiFi router that your ISP sent you when you signed up.  It might, or it might not be a separate component from your Internet modem - you’re better off if it is separate.</p>

<p>Let’s look at what your network looks like with this equipment:</p>

<p><img src="/assets/homelab/ISPRouter.png" alt="Home Network" /></p>

<p>The problem is twofold:</p>

<ol>
  <li>The WiFi router is integrated with the internet router.</li>
  <li>The consumer grade, plug and play, equipment cannot be configured adequately.</li>
</ol>

<p>These two items mean that the equipment is great for people who don’t have the technical knowledge to do much more that plug the bits together, and who can’t figure out how to even change the WiFi password.  And that’s probably the vast, vast majority of customers that the ISP’s are dealing with.</p>

<p>But if you are building your own, self-hosted, services, you’ll need something different.  For instance, with a standard, consumer, WiFi router, you’ll probably have a single “guest” SSID avaliable, and it might even allow you to associate it with a VLAN.  But if you really want to use VLAN’s and subnets properly, you’ll need a few more SSID’s.  Also, if you are implementing your own firewall, you’ll probably need to turn off all of the routing in your WiFi router, along with things like NAT and DHCP.  You’ll probably need to put it into “Bridge” or “AP” mode if that’s possible.</p>

<p>The WiFi router that my latest IP sent me is so simple it doesn’t even have a “Bridge” mode.  I didn’t even bother with it so it’s just stayed in the box in a closet.</p>

<p>Generally speaking, you’ll want to replace your WiFi router with a WiFi “Access Point” or “AP”.  As soon as you go looking for these, you’ll discover that you’re not in the consumer marketplace any more.  Marketting bumpf will talk about how good the devices are for conference rooms or multi-unit dwellings.  Access points are fairly cheap, too.</p>

<h1 id="setting-up-opnsense-to-use-vlans">Setting up OPNSense to Use VLAN’s</h1>

<p>Let’s take a quick survey of how you would set up OPNSense to enable VLAN’s and subnets on your network with just two physical ethernet ports on your server, one of which is the WAN port - and we won’t talk about that.  This isn’t a step-by-step guide, but really just an overview of the things that you need to achieve in order to implement VLAN’s and subnets.  If you want step-by-step instructions, you can find tons of them on YouTube.</p>

<h2 id="creating-vlan-interfaces">Creating VLAN Interfaces</h2>

<p>When you start out, you’ll have an “Interface” configured for the physical internal ethernet port.  This is generally called “LAN”.  You’ll neet to create an Interface for each VLAN that you are going to set up.</p>

<h3 id="creating-the-vlan-device">Creating the VLAN Device</h3>

<p>Before you can create an Interface, you’ll need to set up a VLAN device.  In the “Interfaces” menu there is a submenu called “Devices” and an item in that called “VLAN”.  Click on that option and you’ll get a list of all of the VLAN devices that have already been defined, and then click on the “+” button to add a new one.</p>

<p>You’ll get a screen that looks like this:</p>

<p><img src="/assets/homelab/OPNS_VLAN1.png" alt="VLAN Device" /></p>

<p>The device name is a bit tricky, it needs to start with “vlan0” and then have some more numeric stuff after that.  The parent should be the device associated with your <code class="language-plaintext highlighter-rouge">LAN</code> interface.  Then you give it whatever VLAN tag that you are going to use in your switches.  Give it a description that makes sense to you.</p>

<h3 id="assigning-the-vlan-device-to-an-interface">Assigning the VLAN Device to an Interface</h3>

<p>The next step seems like something that should probably happen automatically, but doesn’t.  In the “Interfaces” menu there is an item called “Assignments”.  Click on that and you’ll get a screen with a list of devices that have been assigned to interfaces.  Down at the bottom is a little section to add new assignments:</p>

<p><img src="/assets/homelab/OPNS_VLAN2.png" alt="VLAN Assignment Creation" /></p>

<p>There’s not much to it.  Just a dropdown and a textbox for the description.  Note that this description is what you are going to see all the time when you deal with just about anything else in OPNSense.  So pick a name that makes sense to you.</p>

<p>At this point, you will have created an Interface for your VLAN!  But it still needs to be configured.</p>

<h3 id="connecting-your-vlan-to-a-subnet">Connecting Your VLAN to a Subnet</h3>

<p>Once you’ve hit “Apply”, you’ll get a new item in the “Interfaces” menu with the name of your Interface.  Go ahead and click on it:</p>

<p><img src="/assets/homelab/OPNS_VLAN3.png" alt="Interface Configuration" /></p>

<p>Not shown here is the checkbox to enable the Interface.  Be sure to click on that to enable the Interface.</p>

<p>I’m just using IPv4 here, so you I’m not going to do any IPv6.  Select “Static” for the IPv4 configuration type and then you’ll see the box at the bottom with address textbox.</p>

<p>That address textbox <em>is</em> where you connect the VLAN to the IP subnet.  You are not just giving the Interface an address, you are defining the subnet to which it belongs.  That’s what the dropdown box to the right is for.  I picked “24” because I want <code class="language-plaintext highlighter-rouge">10.122.73.0</code> to be the subnet, with a mask of <code class="language-plaintext highlighter-rouge">255.255.255.0</code>.</p>

<p>That’s all you need to specify: enable, static IPv4, address and mask.  Hit “Save” and then “Apply”.</p>

<p>Now your Interface and VLAN is set up and ready to go.  You’ll see that new Interface name pop up all over the OPNSense UI whenever it’s an option.  Most importantly, it will be an option in the <code class="language-plaintext highlighter-rouge">Firewall --&gt; Rules</code> menu.  Don’t forget that at this point, it has no rules except the default, “Deny Everything” rule, meaning that this subnet is 100% sequestered for outgoing traffic.</p>

<h2 id="setting-up-dhcp">Setting Up DHCP</h2>

<p>The last step is to configure the OPNSense DHCP service to listen on your new Interface/VLAN.</p>

<p>At the time that I’m writing this, the older DHCP services are being phase out, and KEA is probably overkill for simple home networks.  Which means that you should be using <code class="language-plaintext highlighter-rouge">Dnsmasq DHCP</code>.  So that’s what I’ll show here.</p>

<p>The DHCP configuration is in the “Services” menu.  The first thing you’ll need to do is some basic configuration for the service.  Click on <code class="language-plaintext highlighter-rouge">Services --&gt; Dnsmasq DNS &amp; DHCP</code> to open up the submenu and then click on <code class="language-plaintext highlighter-rouge">General</code>.  You’ll get a screen that will allow you to enable the service, and then select all of the Interfaces that you want it to work with.  Save this and you’re ready to set up your subnets.</p>

<h3 id="setting-up-dhcp-options">Setting Up DHCP Options</h3>

<p>Just as when you set up static IP on a device where you need to specify subnet and mask, gateway and DNS servers along with device IP address, DHCP has to provide the same information.  The subnet and mask are handled automatically, but you need to configure the rest.</p>

<p>In OPNSense Dmasq the gateway and the DNS servers are treated as “options”.  You’ll need to configure these for each Interface.  There’s actually a fair number of options available, and you’ll have to pick the correct two.</p>

<p>In the DMasq screen in OPNSense there’s a tab (and a menu item) called “DHCP Options”.  This will show a list of the options that you have configured for each Interface.  There’s the usual “+” button at the bottom right.  Click it and you’ll see a dialogue like this:</p>

<p><img src="/assets/homelab/OPNS_VLAN4.png" alt="DHCP Options" /></p>

<p>Pick the Interface for your VLAN and select “set” as the action.  You need to do this twice.  Once for “router [3]” as default gateway address, and once for “dns-server [6]”.  It’s probably a good idea to specify a description so that you can see what they are at a glance from the main listing.</p>

<h2 id="thats-it">That’s It</h2>

<p>At this point, you have your firewall configured to support your VLAN’s.  It’s beyond the scope of this article to talk about firewall rules, but just remember that each of these subnets is automatically created in a fully sequestered mode.  Nothing can get out, and nothing can get in.  You’ll have to add the rules to allow devices on these subnets to communicate outside the subnet.</p>

<h3 id="dont-forget-the-switch">Don’t Forget the Switch</h3>

<p>Whatever switch port that <code class="language-plaintext highlighter-rouge">LAN</code> on your OPNSense firewall is plugged into needs to be configured to allow traffic tagged with every VLAN that you set up in the firewall to pass through.  Otherwise, none of this will work.</p>

<h1 id="ignoring-vlans">Ignoring VLAN’s</h1>

<p>The one caveat to all of this is that once a subnet has been connected to a VLAN, nothing outside that VLAN can <em>directly</em> communicate with any address on that subnet that is on the VLAN.</p>

<p>Let’s say that you have three devices on ports 1, 2 and 3 of a managed switch.  Ports 1 and 2 are configured to use VLAN <code class="language-plaintext highlighter-rouge">77</code> for untagged packets while port three is left at the deault configuration, essentially meaning no VLAN processing.  The devices are all configured statically to have IP addresses <code class="language-plaintext highlighter-rouge">10.10.10.10</code>, <code class="language-plaintext highlighter-rouge">10.10.10.11</code> and <code class="language-plaintext highlighter-rouge">10.10.10.12</code> in order in ports 1, 2 and 3.  In this scenario, the devices in ports 1 and 2 can communicate, but the device in port 3 cannot communicate with either one of them, even though it is on the same subnet.</p>

<p>Furthermore, that device on port 3 can only communicate with devices that are on the same subnet and also untagged.  This is because the gateway - presumably at <code class="language-plaintext highlighter-rouge">10.10.10.1</code> - is going to also be on VLAN <code class="language-plaintext highlighter-rouge">77</code> if everything is configured correctly.  This means that the device on port 3 cannot communicate with the default gateway for its subnet, and therefore cannot communicate with any device on another subnet.</p>

<p>At the same time, the devices on ports 1 and 2 can communicate with any device on any other subnet reachable by the firewall, regardless of what VLAN they are on.  This is because the traffic leaving the firewall Interface on the other subnet is going to be tagged with the correct VLAN id for that subnet.</p>

<p>You can see that it’s important that once you start using VLAN’s to then use them everywhere, especially if you are configuring devices on subnets that aren’t associated with <code class="language-plaintext highlighter-rouge">VLAN1</code>, the default “No VLAN” VLAN.</p>

<h1 id="conclusion">Conclusion</h1>

<p>The first thing to remember is that network security <em>behind</em> your firewall is every bit as important as security between your firewall and the outside world.</p>

<p>It’s incredibly easy to hear about VLAN’s and how important they are to your network security, but it’s much harder to find out <em>how</em> they do that.  I’m hoping that this article helps to clear that up.  But to recap:</p>

<p>The core of network security is based on controlling TCP/IP traffic.  That control is virtually always achieved via a firewall to regulate traffic between IP subnets.  VLAN’s work inside your network switches by restricting whether or not data packets will be delivered to particular switch ports and each port on a managed switch is configured such that it will accept packets (incoming or outgoing) with particular VLAN “tags”.  Each port can also be configured to assign a specific tag to incoming packets that have no tags on them.</p>

<p>Your firewall is the component that connects VLAN tagging with TCP/IP subnets and TCP/IP traffic control, which, once again is the main way that you implement your security.  Each VLAN effectively becomes synonymous with a TCP/IP subnet.</p>

<p>You use this technology to divide your internal network up into subnets that represent different “zones”.  Those zones, in turn, hold devices that share a similar trust profile and they are isolated from other zones via your firewall rules.</p>]]></content><author><name>Dave Barrett</name><email>pragmaticcoding8@gmail.com</email></author><category term="homelab" /><summary type="html"><![CDATA[How to secure your home network from threats from inside the house.]]></summary></entry><entry><title type="html">ObservableLists: Extractors</title><link href="https://www.pragmaticcoding.ca/javafx/elements/extractors" rel="alternate" type="text/html" title="ObservableLists: Extractors" /><published>2025-08-05T17:00:00+00:00</published><updated>2025-08-05T17:00:00+00:00</updated><id>https://www.pragmaticcoding.ca/javafx/elements/extractors</id><content type="html" xml:base="https://www.pragmaticcoding.ca/javafx/elements/extractors"><![CDATA[<h1 id="introduction">Introduction</h1>

<p><code class="language-plaintext highlighter-rouge">ObservableLists</code> (and <code class="language-plaintext highlighter-rouge">ObservableSets</code>, and <code class="language-plaintext highlighter-rouge">ObservableMaps</code>, for that matter), are designed to invalidate and trigger <code class="language-plaintext highlighter-rouge">Listeners</code> when items are add or removed from them.  Screen elements like <code class="language-plaintext highlighter-rouge">ListView</code>, <code class="language-plaintext highlighter-rouge">TableView</code> and the pop-ups in <code class="language-plaintext highlighter-rouge">ComboBox</code> will automatically update when the <code class="language-plaintext highlighter-rouge">ObservableLists</code> that back them are updated.</p>

<p>But what about those cases where your <code class="language-plaintext highlighter-rouge">ObservableLists</code> are made of up composed objects?  Is it possible to detect that an <code class="language-plaintext highlighter-rouge">ObservableList</code> has changed when one of the fields inside an item has changed?</p>

<p>That’s what “extractors” are for.  They allow you to detect changes to <code class="language-plaintext highlighter-rouge">Property</code> fields inside the items in your list, and to trigger <code class="language-plaintext highlighter-rouge">Listeners</code> on the <code class="language-plaintext highlighter-rouge">ObservableList</code> as a result.</p>

<h1 id="the-basics">The Basics</h1>

<p>The JavaDocs for extractors is pretty thin.</p>

<p>The first thing that you’ll notice if you check out the entry for <a href="https://openjfx.io/javadoc/23/javafx.base/javafx/collections/ObservableList.html">ObservableList</a> is that there are no constructors for <code class="language-plaintext highlighter-rouge">ObservableList</code> because it’s an <code class="language-plaintext highlighter-rouge">Interface</code>.  None of the “All Known Implementing Classes” section look promising either.</p>

<p>Typically, you’ll get an <code class="language-plaintext highlighter-rouge">ObservableList</code> because it’s already part of a <code class="language-plaintext highlighter-rouge">Node</code> class like <code class="language-plaintext highlighter-rouge">ListView</code>, <code class="language-plaintext highlighter-rouge">TableView</code> or <code class="language-plaintext highlighter-rouge">ComboBox</code>.  But if you want to create one yourself, you’ll need to use the static library class, <code class="language-plaintext highlighter-rouge">FXCollections</code>.</p>

<p>If you go to the JavaDocs for <a href="https://openjfx.io/javadoc/23/javafx.base/javafx/collections/FXCollections.html">FXCollections</a> you’ll see that there are lots and lots of ways to create different kinds of <code class="language-plaintext highlighter-rouge">ObservableLists</code>, some of which specify extractors.  Let’s look at the details for <code class="language-plaintext highlighter-rouge">observableArrayList(Callback&lt;E,Observable[]&gt; extractor)</code>:</p>

<blockquote>
  <p><strong>public static &lt;E&gt; ObservableList&lt;E&gt; observableArrayList(Callback&lt;E,Observable[]&gt; extractor)</strong></p>

  <p>Creates a new empty ObservableList that is backed by an array list and listens to changes in observables of its items.</p>

  <p>The extractor returns observables (usually properties) of the objects in the created list. These observables are listened for changes and the user is notified of these through an update change of an attached ListChangeListener. These changes are unrelated to the changes made to the observable list itself using methods such as add and remove.</p>

  <p>For example, a list of Shapes can listen to changes in the shapes’ fill property.</p>
</blockquote>

<p>This seems to hint at something interesting, but it doesn’t really explain much, does it?</p>

<p>Let’s take a closer look at the constructor parameter: <code class="language-plaintext highlighter-rouge">Callback&lt;E,Observable[]&gt; extractor</code>.</p>

<p>A <code class="language-plaintext highlighter-rouge">Callback</code> is just a <code class="language-plaintext highlighter-rouge">Function</code>, meaning that it accepts one value and then returns another.  The name “Callback” implies that it is going to be used somewhere deep down in the internals of something.  Somewhere that we can’t see, and probably don’t want to see.  The <code class="language-plaintext highlighter-rouge">Callback</code> is a “hook” to allow that hidden code to get at something that we, the application programmers, define.</p>

<p>In this case, the <code class="language-plaintext highlighter-rouge">Callback</code> accepts something of type <code class="language-plaintext highlighter-rouge">E</code>.  What is <code class="language-plaintext highlighter-rouge">E</code>? Well, <code class="language-plaintext highlighter-rouge">ObservableList</code> itself is generic, and <code class="language-plaintext highlighter-rouge">E</code> in this case refers to the type of elements that comprise our <code class="language-plaintext highlighter-rouge">ObservableList</code>.  This means that the <code class="language-plaintext highlighter-rouge">Callback</code> is just going to accept a single element of our <code class="language-plaintext highlighter-rouge">ObservableList</code>, whatever that happens to be.</p>

<p>The output from our <code class="language-plaintext highlighter-rouge">Callback</code> is going to be an <code class="language-plaintext highlighter-rouge">array</code> of <code class="language-plaintext highlighter-rouge">Observable</code>.  <code class="language-plaintext highlighter-rouge">Observable</code> is the top level <code class="language-plaintext highlighter-rouge">Interface</code> that all of the <code class="language-plaintext highlighter-rouge">Properties</code> and observable classes implement.  It only specifies three methods, <code class="language-plaintext highlighter-rouge">addListener()</code>, <code class="language-plaintext highlighter-rouge">removeListener()</code> and <code class="language-plaintext highlighter-rouge">subscribe()</code>.  That last one, <code class="language-plaintext highlighter-rouge">subscribe()</code> is new, and <code class="language-plaintext highlighter-rouge">ObservableList</code> extractors pre-dates it.  So we don’t need to worry about it here.</p>

<p>What this tells us is that at some point, deep inside the <code class="language-plaintext highlighter-rouge">ObservableList</code> code, JavaFX is going to add a <code class="language-plaintext highlighter-rouge">Listener</code> to every <code class="language-plaintext highlighter-rouge">Observable</code> that we return for every item in our <code class="language-plaintext highlighter-rouge">ObservableList</code>.</p>

<p>The only question left is: What does that <code class="language-plaintext highlighter-rouge">Listener</code> do?</p>

<h1 id="what-does-the-extractor-do">What Does the Extractor Do?</h1>

<p>In my article about <a href="/javafx/elements/observable-classes-lists">ObservableLists</a>, part of my <a href="/javafx/elements/observables_guide">series on Observables</a>, I wrote some code to see what kind of <code class="language-plaintext highlighter-rouge">Listeners</code> are activated when changes are made to <code class="language-plaintext highlighter-rouge">ObservableLists</code>.  This involved adding, deleting, replacing and swapping <code class="language-plaintext highlighter-rouge">ObservableList</code> elements.</p>

<p>We can use some of that same code here, to see how extractors translate to the <code class="language-plaintext highlighter-rouge">Listeners</code> triggered the <code class="language-plaintext highlighter-rouge">ObservableList</code>.</p>

<div class="notice--kotlin">
 <img src="/assets/logos/Kotlin.png" alt="Kotlin" style="float:left;margin-right: 10px;margin-top: 8px;" />
 <p style="overflow:auto; float:none">
   While code is this article is written in Kotlin, all of the JavaFX concepts are exactly the same.
   Most of the Kotlin should be intuitively obvious to Java programmers,
   but if you need help understanding it, refer to this <a href="/kotlin/kotlin-examples" title="Read the article" target="_blank">page</a>.
 </p>
</div>

<p>To start, we’ll look at the code without an extractor, and see what it does…</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="kd">val</span> <span class="py">obList</span><span class="p">:</span> <span class="nc">ObservableList</span><span class="p">&lt;</span><span class="nc">ExampleData</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">FXCollections</span><span class="p">.</span><span class="nf">observableArrayList</span><span class="p">()</span>
<span class="k">private</span> <span class="kd">val</span> <span class="py">messages</span><span class="p">:</span> <span class="nc">StringProperty</span> <span class="p">=</span> <span class="nc">SimpleStringProperty</span><span class="p">(</span><span class="s">""</span><span class="p">)</span>
<span class="k">private</span> <span class="kd">val</span> <span class="py">totBinding</span> <span class="p">=</span> <span class="kd">object</span> <span class="err">: </span><span class="nc">IntegerBinding</span><span class="p">()</span> <span class="p">{</span>
    <span class="nf">init</span> <span class="p">{</span>
        <span class="k">super</span><span class="p">.</span><span class="nf">bind</span><span class="p">(</span><span class="n">obList</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">computeValue</span><span class="p">():</span> <span class="nc">Int</span> <span class="p">=</span> <span class="n">obList</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">}.</span><span class="nf">sum</span><span class="p">()</span>
<span class="p">}</span>

<span class="k">override</span> <span class="k">fun</span> <span class="nf">start</span><span class="p">(</span><span class="n">stage</span><span class="p">:</span> <span class="nc">Stage</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">obList</span><span class="p">.</span><span class="nf">subscribe</span> <span class="p">{</span> <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"Invalidated \n"</span> <span class="p">}</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">x</span> <span class="k">in</span> <span class="mi">1</span><span class="o">..</span><span class="mi">5</span><span class="p">)</span> <span class="n">obList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">ExampleData</span><span class="p">(</span><span class="n">x</span><span class="p">))</span>
    <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"Data Loaded\n"</span>
    <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="n">obList</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">}.</span><span class="nf">joinToString</span><span class="p">()</span> <span class="p">+</span> <span class="s">"\n"</span>
    <span class="n">stage</span><span class="p">.</span><span class="n">scene</span> <span class="p">=</span> <span class="nc">Scene</span><span class="p">(</span><span class="nf">createContent</span><span class="p">()).</span><span class="nf">apply</span> <span class="p">{</span> <span class="p">}</span>
    <span class="n">stage</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
<span class="p">}</span>

<span class="k">private</span> <span class="k">fun</span> <span class="nf">createContent</span><span class="p">():</span> <span class="nc">Region</span> <span class="p">=</span> <span class="nc">BorderPane</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
    <span class="n">top</span> <span class="p">=</span> <span class="nc">HBox</span><span class="p">(</span>
        <span class="mf">10.0</span><span class="p">,</span>
        <span class="nc">Label</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span> <span class="nf">textProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="n">totBinding</span><span class="p">.</span><span class="nf">asString</span><span class="p">())</span> <span class="p">},</span>
        <span class="nc">Label</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span> <span class="nf">textProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="n">obList</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="n">value2</span><span class="p">.</span><span class="nf">asString</span><span class="p">())</span> <span class="p">})</span>
    <span class="n">center</span> <span class="p">=</span> <span class="nc">TextArea</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="nf">textProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="n">messages</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="n">bottom</span> <span class="p">=</span> <span class="nc">Button</span><span class="p">(</span><span class="s">"Increment Item"</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="nf">setOnAction</span> <span class="p">{</span>
            <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"Button Clicked\n"</span>
            <span class="nf">with</span><span class="p">(</span><span class="n">obList</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span> <span class="p">{</span> <span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">+</span> <span class="mi">1</span> <span class="p">}</span>
            <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="n">obList</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">}.</span><span class="nf">joinToString</span><span class="p">()</span> <span class="p">+</span> <span class="s">"\n"</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="n">padding</span> <span class="p">=</span> <span class="nc">Insets</span><span class="p">(</span><span class="mf">20.0</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>

<span class="kd">class</span> <span class="nc">ExampleData</span><span class="p">(</span><span class="n">initialValue</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">value1</span><span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="n">initialValue</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">value2</span><span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="n">initialValue</span><span class="p">)</span>
<span class="p">}</span>


<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nc">Application</span><span class="p">.</span><span class="nf">launch</span><span class="p">(</span><span class="nc">ExtractorExample1</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">)</span>
</code></pre></div></div>
<p>We have an <code class="language-plaintext highlighter-rouge">ObservableList</code> of <code class="language-plaintext highlighter-rouge">DataExample</code>, which is just an object with two <code class="language-plaintext highlighter-rouge">IntegerProperties</code> in it.  We populate it with 5 items, where both <code class="language-plaintext highlighter-rouge">IntegerProperties</code> are populated with the same value and that value increments for each item that we add.</p>

<p>The GUI is a <code class="language-plaintext highlighter-rouge">BorderPane</code> with an <code class="language-plaintext highlighter-rouge">HBox</code> at the top with two <code class="language-plaintext highlighter-rouge">Labels</code>.  One shows the total of all of <code class="language-plaintext highlighter-rouge">DataExample.value2</code> in the <code class="language-plaintext highlighter-rouge">ObservableList</code> through a <code class="language-plaintext highlighter-rouge">Binding</code> and the other has the current value of <code class="language-plaintext highlighter-rouge">obList[2].value2</code>.</p>

<p>The centre is a <code class="language-plaintext highlighter-rouge">TextArea</code> which is bound to a <code class="language-plaintext highlighter-rouge">StringProperty</code> called <code class="language-plaintext highlighter-rouge">messages</code>.  Various bits of code add new data to <code class="language-plaintext highlighter-rouge">messages</code>, and we’ll look at this.</p>

<p>At the bottom is a <code class="language-plaintext highlighter-rouge">Button</code>.  When this <code class="language-plaintext highlighter-rouge">Button</code> is clicked three things happen:</p>

<ol>
  <li>“Button Clicked” is added to <code class="language-plaintext highlighter-rouge">messages</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">obList[2].value2</code> is incremented.</li>
  <li>A list of all of the values in <code class="language-plaintext highlighter-rouge">value2</code> is added to <code class="language-plaintext highlighter-rouge">messages</code></li>
</ol>

<p>We also have an <code class="language-plaintext highlighter-rouge">InvalidationListener</code> (through <code class="language-plaintext highlighter-rouge">subscribe()</code>) on <code class="language-plaintext highlighter-rouge">obList</code> that just adds “Invalidated” to <code class="language-plaintext highlighter-rouge">messages</code>.  And we add “Data Loaded” to <code class="language-plaintext highlighter-rouge">messages</code> when all of the setup is done.</p>

<p>And all of this looks like this:</p>

<p><img src="/assets/elements/Extractors2.png" alt="Screen Capture 2" /></p>

<p>You can see that <code class="language-plaintext highlighter-rouge">obList</code> invalidates every time a new <code class="language-plaintext highlighter-rouge">ExampleData</code> is added to it.  Then the “Data Loaded” message is appended and we see the initial list of values in <code class="language-plaintext highlighter-rouge">value2</code>.</p>

<p>Then we see that the <code class="language-plaintext highlighter-rouge">Button</code> was clicked, and that <code class="language-plaintext highlighter-rouge">obList[2].value2</code> was incremented.  And then again, and again, and again.</p>

<p>We can also see that our <code class="language-plaintext highlighter-rouge">Label</code> bound to <code class="language-plaintext highlighter-rouge">obList[2].value2</code> has been updated, and it shows “7” - just as you would expect.</p>

<p>However, the other <code class="language-plaintext highlighter-rouge">Label</code> hasn’t been updated.  This is the one bound to the <code class="language-plaintext highlighter-rouge">ObservableList</code> itself.  Also, out <code class="language-plaintext highlighter-rouge">InvalidationListener</code> never fired again.</p>

<div class="notice--primary">
 <img src="/assets/logos/brain.png" alt="primary" style="float:left;margin-right: 10px" />
 <p style="overflow:auto; float:none">
   When ObservableList items are composed objects containing Observable type fields, changes to those internal fields will not trigger Listeners on the ObservableList.
 </p>
</div>

<h2 id="adding-an-extractor">Adding an Extractor</h2>

<p>All we are going to do is change a single line of the code from above.  This:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="kd">val</span> <span class="py">obList</span><span class="p">:</span> <span class="nc">ObservableList</span><span class="p">&lt;</span><span class="nc">ExampleData</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">FXCollections</span><span class="p">.</span><span class="nf">observableArrayList</span><span class="p">()</span>
</code></pre></div></div>
<p>will change to this:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="kd">val</span> <span class="py">obList</span><span class="p">:</span> <span class="nc">ObservableList</span><span class="p">&lt;</span><span class="nc">ExampleData</span><span class="p">&gt;</span> <span class="p">=</span>
    <span class="nc">FXCollections</span><span class="p">.</span><span class="nf">observableArrayList</span><span class="p">({</span> <span class="n">item</span> <span class="p">-&gt;</span> <span class="nf">arrayOf</span><span class="p">(</span><span class="n">item</span><span class="p">.</span><span class="n">value2</span><span class="p">)</span> <span class="p">})</span>
</code></pre></div></div>
<p>In this code, <code class="language-plaintext highlighter-rouge">{ item -&gt; arrayOf(item.value2) }</code> is the Callback for the extractor.  It creates an array, and it includes <code class="language-plaintext highlighter-rouge">ExampleData.value2</code> in that array.</p>

<p>Now, when we run the program and click the <code class="language-plaintext highlighter-rouge">Button</code> four times, we get this:</p>

<p><img src="/assets/elements/Extractors3.png" alt="Screen Capture 3" /></p>

<p>The first thing that you notice is that we get the “Invalidated” message right after every “Button Clicked” message.  This means that the <code class="language-plaintext highlighter-rouge">InvalidationListener</code> on <code class="language-plaintext highlighter-rouge">obList</code> has fired!</p>

<p>The other thing that you should notice is that the first <code class="language-plaintext highlighter-rouge">Label</code> now says “19”, which is 1+2+7+4+5.  This means that the <code class="language-plaintext highlighter-rouge">Binding</code> on <code class="language-plaintext highlighter-rouge">obList</code> that sums up the values is now working!</p>

<div class="notice--primary">
 <img src="/assets/logos/brain.png" alt="primary" style="float:left;margin-right: 10px" />
 <p style="overflow:auto; float:none">
   When an ObservableList has an extractor that returns an encapsulated Observable field of the List items, then changes to that internal fields will trigger Listeners on the ObservableList.
 </p>
</div>

<p>This tells us that, at a minumum, the <code class="language-plaintext highlighter-rouge">InvalidationListener</code> that the <code class="language-plaintext highlighter-rouge">ObservableList</code> puts on every <code class="language-plaintext highlighter-rouge">value2</code> of every <code class="language-plaintext highlighter-rouge">ExampleData</code> in the list is used to trigger an <code class="language-plaintext highlighter-rouge">InvalidationListener</code> on the <code class="language-plaintext highlighter-rouge">ObservableList</code> itself.  That’s enough to trigger the <code class="language-plaintext highlighter-rouge">Binding</code> on the <code class="language-plaintext highlighter-rouge">ObservableList</code>, too.</p>

<p>But does it do more?</p>

<h1 id="will-a-listchangelistener-work">Will a ListChangeListener Work?</h1>

<p>Let’s change the code a little bit to add a <code class="language-plaintext highlighter-rouge">ListChangeListener</code> to <code class="language-plaintext highlighter-rouge">obList</code> to see if the extractor will cause it to fire:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">override</span> <span class="k">fun</span> <span class="nf">start</span><span class="p">(</span><span class="n">stage</span><span class="p">:</span> <span class="nc">Stage</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">obList</span><span class="p">.</span><span class="nf">subscribe</span> <span class="p">{</span> <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"Invalidated \n"</span> <span class="p">}</span>
    <span class="n">obList</span><span class="p">.</span><span class="nf">addListener</span><span class="p">(</span><span class="nc">ListChangeListener</span> <span class="p">{</span> <span class="n">change</span> <span class="p">-&gt;</span>
        <span class="k">while</span> <span class="p">(</span><span class="n">change</span><span class="p">.</span><span class="nf">next</span><span class="p">())</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">change</span><span class="p">.</span><span class="nf">wasPermutated</span><span class="p">())</span> <span class="p">{</span>
                <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"List was permutated\n"</span>
            <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">change</span><span class="p">.</span><span class="nf">wasRemoved</span><span class="p">())</span> <span class="p">{</span>
                    <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"List item was removed\n"</span>
                <span class="p">}</span>

                <span class="k">if</span> <span class="p">(</span><span class="n">change</span><span class="p">.</span><span class="nf">wasAdded</span><span class="p">())</span> <span class="p">{</span>
                    <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"List item was added\n"</span>
                <span class="p">}</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">change</span><span class="p">.</span><span class="nf">wasUpdated</span><span class="p">())</span> <span class="p">{</span>
                   <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"List item ${change.from} was updated\n"</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">})</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">x</span> <span class="k">in</span> <span class="mi">1</span><span class="o">..</span><span class="mi">5</span><span class="p">)</span> <span class="n">obList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">ExampleData</span><span class="p">(</span><span class="n">x</span><span class="p">))</span>
    <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"Data Loaded\n"</span>
    <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="n">obList</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">}.</span><span class="nf">joinToString</span><span class="p">()</span> <span class="p">+</span> <span class="s">"\n"</span>
    <span class="n">stage</span><span class="p">.</span><span class="n">scene</span> <span class="p">=</span> <span class="nc">Scene</span><span class="p">(</span><span class="nf">createContent</span><span class="p">()).</span><span class="nf">apply</span> <span class="p">{</span> <span class="p">}</span>
    <span class="n">stage</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">ListChangeListener</code> accepts a value of <code class="language-plaintext highlighter-rouge">ListChangeListener.Change</code>, which itself is an iterable construct and may contain several different changes to the <code class="language-plaintext highlighter-rouge">ObservableList</code> that happened at the same time.  These changes can be that an item (or range of items) were removed, added or permutated (which means they were replaced).  We need code that will look at all of the types of changes to see what is happening.</p>

<p>Again, we’ll run the code and click on the <code class="language-plaintext highlighter-rouge">Button</code> a few times:</p>

<p><img src="/assets/elements/Extractors4.png" alt="Screen Capture 4" /></p>

<p>Here, we can see that the <code class="language-plaintext highlighter-rouge">ListChangeListener</code> fires as each item is added to the <code class="language-plaintext highlighter-rouge">ObservableList</code>, but then it fires with an “update” <code class="language-plaintext highlighter-rouge">Change</code> when the <code class="language-plaintext highlighter-rouge">Button</code> is clicked.  It also reports which element was updated in <code class="language-plaintext highlighter-rouge">Change.from</code>.</p>

<p>From this, we can safely assume that a <code class="language-plaintext highlighter-rouge">ListChangeListener</code> is going to detect a change made to a field specified in an extractor.</p>

<h1 id="will-a-listproperty-work">Will a ListProperty Work?</h1>

<p>There is another class, called <code class="language-plaintext highlighter-rouge">ListProperty</code> that acts as a wrapper around an <code class="language-plaintext highlighter-rouge">ObservableList</code>.  What if we put our <code class="language-plaintext highlighter-rouge">obList</code> into a <code class="language-plaintext highlighter-rouge">ListProperty</code> and then add a <code class="language-plaintext highlighter-rouge">ChangeListener</code> to that <code class="language-plaintext highlighter-rouge">ListProperty</code>?</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">override</span> <span class="k">fun</span> <span class="nf">start</span><span class="p">(</span><span class="n">stage</span><span class="p">:</span> <span class="nc">Stage</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">listProperty</span> <span class="p">=</span> <span class="nc">SimpleListProperty</span><span class="p">(</span><span class="n">obList</span><span class="p">)</span>
    <span class="n">listProperty</span><span class="p">.</span><span class="nf">addListener</span><span class="p">(</span><span class="nc">InvalidationListener</span> <span class="p">{</span> <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"Invalidated \n"</span> <span class="p">})</span>
    <span class="n">listProperty</span><span class="p">.</span><span class="nf">subscribe</span> <span class="p">{</span> <span class="n">oldVal</span><span class="p">,</span> <span class="n">newVal</span> <span class="p">-&gt;</span>
        <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"Change -&gt; Old: $oldVal  New: $newVal\n"</span>
    <span class="p">}</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">x</span> <span class="k">in</span> <span class="mi">1</span><span class="o">..</span><span class="mi">5</span><span class="p">)</span> <span class="n">obList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">ExampleData</span><span class="p">(</span><span class="n">x</span><span class="p">))</span>
    <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"Data Loaded\n"</span>
    <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="n">obList</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">}.</span><span class="nf">joinToString</span><span class="p">()</span> <span class="p">+</span> <span class="s">"\n"</span>
    <span class="n">stage</span><span class="p">.</span><span class="n">scene</span> <span class="p">=</span> <span class="nc">Scene</span><span class="p">(</span><span class="nf">createContent</span><span class="p">()).</span><span class="nf">apply</span> <span class="p">{</span> <span class="p">}</span>
    <span class="n">stage</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Notice that I’ve also moved the <code class="language-plaintext highlighter-rouge">InvalidationListener</code> to the <code class="language-plaintext highlighter-rouge">ListProperty</code>, so that we can see if it fires on there as well.</p>

<p>When we run this, clicking the <code class="language-plaintext highlighter-rouge">Button</code> four times, we get:</p>

<p><img src="/assets/elements/Extractors5.png" alt="Screen Capture 5" /></p>

<p>We see the <code class="language-plaintext highlighter-rouge">ChangeListener</code> firing as the elements are added, and this not suprising.  You will notice, however, thet the <code class="language-plaintext highlighter-rouge">oldVal</code> and <code class="language-plaintext highlighter-rouge">newVal</code> are both actually the <code class="language-plaintext highlighter-rouge">newVal</code>.  This is a quirk of <code class="language-plaintext highlighter-rouge">ChangeListeners</code> on <code class="language-plaintext highlighter-rouge">ListProperty</code>, and has nothing to do with extractors.</p>

<p>Then we see the <code class="language-plaintext highlighter-rouge">ChangeListener</code> also fires on the changes to the enclosed field through the extractor.  This is interesting, although we still don’t get to see the correct value for <code class="language-plaintext highlighter-rouge">oldVal</code>.</p>

<p>You should also note that the <code class="language-plaintext highlighter-rouge">InvalidationListener</code> also fires every time for the <code class="language-plaintext highlighter-rouge">ListProperty</code>.</p>

<p>And just to make sure this is the extractor that is doing this, I took it out and re-ran the program:</p>

<p><img src="/assets/elements/Extractors6.png" alt="Screen Capture 6" /></p>

<p>Here, we just get the <code class="language-plaintext highlighter-rouge">Listeners</code> firing for the additions to the <code class="language-plaintext highlighter-rouge">ObservableList</code>.  The changes to the enclosed <code class="language-plaintext highlighter-rouge">Property</code> do not trigger any <code class="language-plaintext highlighter-rouge">Listeners</code>.</p>

<h1 id="advanced-extractors">Advanced Extractors</h1>

<p>If you look up extractor examples on-line, you’ll never see anything more than:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Callback</span><span class="o">&lt;</span><span class="nc">TestClass</span><span class="o">,</span> <span class="nc">Observable</span><span class="o">[]&gt;</span> <span class="n">extractor</span> <span class="o">=</span> <span class="n">obj</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">Observable</span><span class="o">[]{</span> <span class="n">obj</span><span class="o">.</span><span class="na">nameProperty</span><span class="o">()</span> <span class="o">};</span>
<span class="nc">ObservableList</span><span class="o">&lt;</span><span class="nc">TestClass</span><span class="o">&gt;</span> <span class="n">list</span> <span class="o">=</span> <span class="nc">FXCollections</span><span class="o">.</span><span class="na">observableArrayList</span><span class="o">(</span><span class="n">extractor</span><span class="o">);</span>
</code></pre></div></div>
<p>I just pulled this example from a StackOverflow question, but it’s pretty much the standard.</p>

<p>But the extractor is a <code class="language-plaintext highlighter-rouge">Callback</code> and can do much more, if you want.  Let’s take a look at that:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">ExtractorExample4</span> <span class="p">:</span> <span class="nc">Application</span><span class="p">()</span> <span class="p">{</span>

    <span class="k">private</span> <span class="kd">val</span> <span class="py">obList</span><span class="p">:</span> <span class="nc">ObservableList</span><span class="p">&lt;</span><span class="nc">ExampleData1</span><span class="p">&gt;</span> <span class="p">=</span>
        <span class="nc">FXCollections</span><span class="p">.</span><span class="nf">observableArrayList</span><span class="p">({</span> <span class="n">item</span> <span class="p">-&gt;</span> <span class="n">item</span><span class="p">.</span><span class="nf">extractableValues</span><span class="p">()</span> <span class="p">})</span>

    <span class="k">private</span> <span class="kd">val</span> <span class="py">messages</span><span class="p">:</span> <span class="nc">StringProperty</span> <span class="p">=</span> <span class="nc">SimpleStringProperty</span><span class="p">(</span><span class="s">""</span><span class="p">)</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">totBinding</span> <span class="p">=</span> <span class="kd">object</span> <span class="err">: </span><span class="nc">IntegerBinding</span><span class="p">()</span> <span class="p">{</span>
        <span class="nf">init</span> <span class="p">{</span>
            <span class="k">super</span><span class="p">.</span><span class="nf">bind</span><span class="p">(</span><span class="n">obList</span><span class="p">)</span>
        <span class="p">}</span>

        <span class="k">override</span> <span class="k">fun</span> <span class="nf">computeValue</span><span class="p">():</span> <span class="nc">Int</span> <span class="p">=</span> <span class="n">obList</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">}.</span><span class="nf">sum</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">start</span><span class="p">(</span><span class="n">stage</span><span class="p">:</span> <span class="nc">Stage</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">obList</span><span class="p">.</span><span class="nf">addListener</span><span class="p">(</span><span class="nc">InvalidationListener</span> <span class="p">{</span> <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"Invalidated \n"</span> <span class="p">})</span>
        <span class="k">for</span> <span class="p">(</span><span class="n">x</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..</span><span class="mi">5</span><span class="p">)</span> <span class="n">obList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">ExampleData1</span><span class="p">(</span><span class="n">x</span><span class="p">))</span>
        <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"Data Loaded\n"</span>
        <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="n">obList</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">}.</span><span class="nf">joinToString</span><span class="p">()</span> <span class="p">+</span> <span class="s">"\n"</span>
        <span class="n">stage</span><span class="p">.</span><span class="n">scene</span> <span class="p">=</span> <span class="nc">Scene</span><span class="p">(</span><span class="nf">createContent</span><span class="p">()).</span><span class="nf">apply</span> <span class="p">{</span> <span class="p">}</span>
        <span class="n">stage</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">createContent</span><span class="p">():</span> <span class="nc">Region</span> <span class="p">=</span> <span class="nc">BorderPane</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="n">top</span> <span class="p">=</span> <span class="nc">HBox</span><span class="p">(</span>
            <span class="mf">10.0</span><span class="p">,</span>
            <span class="nc">Label</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span> <span class="nf">textProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="n">totBinding</span><span class="p">.</span><span class="nf">asString</span><span class="p">())</span> <span class="p">},</span>
            <span class="nc">Label</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span> <span class="nf">textProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="n">obList</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="n">value2</span><span class="p">.</span><span class="nf">asString</span><span class="p">())</span> <span class="p">})</span>
        <span class="n">center</span> <span class="p">=</span> <span class="nc">TextArea</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
            <span class="nf">textProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="n">messages</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="n">bottom</span> <span class="p">=</span> <span class="nc">HBox</span><span class="p">(</span>
            <span class="mf">20.0</span><span class="p">,</span> <span class="nc">Button</span><span class="p">(</span><span class="s">"Increment Item 2"</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
                <span class="nf">setOnAction</span> <span class="p">{</span>
                    <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"Button 1 Clicked\n"</span>
                    <span class="nf">with</span><span class="p">(</span><span class="n">obList</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span> <span class="p">{</span> <span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">+</span> <span class="mi">1</span> <span class="p">}</span>
                    <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="n">obList</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">}.</span><span class="nf">joinToString</span><span class="p">()</span> <span class="p">+</span> <span class="s">"\n"</span>
                <span class="p">}</span>
            <span class="p">},</span>
            <span class="nc">Button</span><span class="p">(</span><span class="s">"Increment Item 5"</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
                <span class="nf">setOnAction</span> <span class="p">{</span>
                    <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"Button 2 Clicked\n"</span>
                    <span class="nf">with</span><span class="p">(</span><span class="n">obList</span><span class="p">[</span><span class="mi">5</span><span class="p">])</span> <span class="p">{</span> <span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">+</span> <span class="mi">1</span> <span class="p">}</span>
                    <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="n">obList</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">}.</span><span class="nf">joinToString</span><span class="p">()</span> <span class="p">+</span> <span class="s">"\n"</span>
                <span class="p">}</span>
            <span class="p">})</span>
        <span class="n">padding</span> <span class="p">=</span> <span class="nc">Insets</span><span class="p">(</span><span class="mf">20.0</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">class</span> <span class="nc">ExampleData1</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">initialValue</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">value1</span><span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="n">initialValue</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">value2</span><span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="n">initialValue</span><span class="p">)</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">toString</span><span class="p">()</span> <span class="p">=</span> <span class="s">"[${value1.value}, ${value2.value}]"</span>

    <span class="k">fun</span> <span class="nf">extractableValues</span><span class="p">():</span> <span class="nc">Array</span><span class="p">&lt;</span><span class="nc">Observable</span><span class="p">&gt;</span> <span class="p">{</span>
        <span class="k">return</span> <span class="k">if</span> <span class="p">(</span><span class="n">initialValue</span> <span class="p">&lt;</span> <span class="mi">4</span><span class="p">)</span> <span class="nf">arrayOf</span><span class="p">(</span><span class="n">value1</span><span class="p">)</span> <span class="k">else</span> <span class="nf">arrayOf</span><span class="p">(</span><span class="n">value2</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>


<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nc">Application</span><span class="p">.</span><span class="nf">launch</span><span class="p">(</span><span class="nc">ExtractorExample4</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">)</span>
</code></pre></div></div>
<p>You’ll notice that we’ve added <code class="language-plaintext highlighter-rouge">ExampleData1</code>, which differs from <code class="language-plaintext highlighter-rouge">ExampleData</code> in two ways:</p>

<ol>
  <li>The constructor parameter <code class="language-plaintext highlighter-rouge">initialValue</code> has now been changed into a private immutable field.</li>
  <li>We’ve have a new method, <code class="language-plaintext highlighter-rouge">extractableValues</code>.</li>
</ol>

<p>This new method will return an <code class="language-plaintext highlighter-rouge">Array</code> containing either <code class="language-plaintext highlighter-rouge">value1</code> or <code class="language-plaintext highlighter-rouge">value2</code> depending on whether <code class="language-plaintext highlighter-rouge">initialValue</code> is less than 4 or not.</p>

<p>Additionally, we’ve added a second <code class="language-plaintext highlighter-rouge">Button</code> that works very much the same way as the original <code class="language-plaintext highlighter-rouge">Button</code> but acts on the 5th item in the <code class="language-plaintext highlighter-rouge">ObservableList</code>, which should have an <code class="language-plaintext highlighter-rouge">initialValue</code> greater than 4.</p>

<p>The <code class="language-plaintext highlighter-rouge">Listeners</code> have been return to very much the way that they were before we introduced the <code class="language-plaintext highlighter-rouge">ListProperty</code>, which has now been removed.</p>

<p>Here’s what the screen looks like when we click the first <code class="language-plaintext highlighter-rouge">Button</code> a few times:</p>

<p><img src="/assets/elements/Extractors7.png" alt="Screen Capture 7" /></p>

<p>You can see the messages from the <code class="language-plaintext highlighter-rouge">Button</code> click, and you can see the updated values in <code class="language-plaintext highlighter-rouge">obList</code>.  You can also see that the second <code class="language-plaintext highlighter-rouge">Label</code> is still correct.  However, the first <code class="language-plaintext highlighter-rouge">Label</code> does NOT have the correct value, and we do not have any “Invalidated” messages for those clicks.</p>

<p>This is expected, because the <code class="language-plaintext highlighter-rouge">initialValue</code> of the item we clicked was “2” which is less than “4” and the extractor pulls <code class="language-plaintext highlighter-rouge">value1</code> for that item, while the <code class="language-plaintext highlighter-rouge">Button</code> is updating <code class="language-plaintext highlighter-rouge">value2</code>.</p>

<p>Here’s what it looks like after we click the new <code class="language-plaintext highlighter-rouge">Button</code> a few times:</p>

<p><img src="/assets/elements/Extractors8.png" alt="Screen Capture 8" /></p>

<p>Now we see the “Invalidated” messages, and the first <code class="language-plaintext highlighter-rouge">Label</code> now contains the correct total.</p>

<h2 id="uses-of-this-technique">Uses of This Technique</h2>

<p>I’ve never seen anyone demonstrate anything like this before, and I’m not sure how often a use would pop up for this in real life.  But there are couple of things to take note of:</p>

<h3 id="the-extraction-details-are-encapsulated">The Extraction Details are Encapsulated</h3>

<p>When you think about it, you see that this approach puts the logic for extraction <em>inside</em> the <code class="language-plaintext highlighter-rouge">ObservableList</code> items.  This means that the nature of the extraction is hidden from the <code class="language-plaintext highlighter-rouge">ObservableList</code> itself.</p>

<p>If you are using a framework with a formal Presentation Model, like MVCI, then you are most likely going to instantiate your <code class="language-plaintext highlighter-rouge">ObservableLists</code> inside the Model.  Defining the extractors as part of that call to <code class="language-plaintext highlighter-rouge">FXCollections.observableArrayList()</code> requires that the Model knows about the structure of the objects that are put inside the <code class="language-plaintext highlighter-rouge">ObservableList</code>.</p>

<p>This might not be appropriate, even though those <code class="language-plaintext highlighter-rouge">ObservableList</code> items are part of the Presentation Model.</p>

<p>Still, it’s worth considering, and I can see some value in putting the logic that dictates how the items will behave in an <code class="language-plaintext highlighter-rouge">ObservableList</code> right beside the actual data that comprises the items.</p>

<h3 id="the-extraction-logic-needs-to-be-immutable">The Extraction Logic Needs to Be Immutable</h3>

<p>You’ll notice that I very deliberately based the extraction logic on a field defined as <code class="language-plaintext highlighter-rouge">val</code> instead of <code class="language-plaintext highlighter-rouge">var</code>.  This is the same as using <code class="language-plaintext highlighter-rouge">final</code> in Java.</p>

<p>This is because the extractor logic is only run once, when the item is added to the <code class="language-plaintext highlighter-rouge">ObservableList</code>.</p>

<p>I checked the source code and found that the extractor code is run to get an <code class="language-plaintext highlighter-rouge">Array</code> of <code class="language-plaintext highlighter-rouge">Observable</code> and then the <code class="language-plaintext highlighter-rouge">ObservableList</code> loops through all of these <code class="language-plaintext highlighter-rouge">Observables</code> and adds an <code class="language-plaintext highlighter-rouge">InvalidationListener</code> to each one.  This code is run when the <code class="language-plaintext highlighter-rouge">ObservableList</code> is created as a wrapper around an existing <code class="language-plaintext highlighter-rouge">List</code>, like this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">obList</span> <span class="p">=</span> <span class="nc">FXCollections</span><span class="p">.</span><span class="nf">observableArrayList</span><span class="p">(</span><span class="n">existingList</span><span class="p">,</span> <span class="n">extractor</span><span class="p">)</span>
</code></pre></div></div>
<p>In this case, the interal code will loop through every item in <code class="language-plaintext highlighter-rouge">existingList</code>, run the <code class="language-plaintext highlighter-rouge">extractor</code> against them and the add the <code class="language-plaintext highlighter-rouge">InvalidationListeners</code> to every <code class="language-plaintext highlighter-rouge">Observable</code> returned by the <code class="language-plaintext highlighter-rouge">extractor</code> for each item.</p>

<p>The extractor code is also run when a new item is added to an existing <code class="language-plaintext highlighter-rouge">ObservableList</code> that has an extractor.</p>

<p>There extractor code is also run when an item is removed from an <code class="language-plaintext highlighter-rouge">ObservableList</code>, in order to remove the <code class="language-plaintext highlighter-rouge">InvalidationListeners</code> that were added by the <code class="language-plaintext highlighter-rouge">ObservableList</code>.</p>

<p>Other than that, the extract isn’t ever run against an item.</p>

<p>If, in our example, we had made <code class="language-plaintext highlighter-rouge">initialValue</code> a var, and provided some way to change it after the items were in the list, there would be no change to <code class="language-plaintext highlighter-rouge">InvalidationListeners</code> added by the <code class="language-plaintext highlighter-rouge">ObservableList</code>, because they were already established.  So, you could end up with a system where the apparent behaviour of the extractor doesn’t match the current state of the data.</p>

<p>The worse case scenario is that if the item is removed from the <code class="language-plaintext highlighter-rouge">ObservableList</code>, then some of the <code class="language-plaintext highlighter-rouge">InvalidationListeners</code> might not be removed because the extractor behaviour wasn’t the same as when the item was added to the <code class="language-plaintext highlighter-rouge">ObservableList</code>.  That could give some weird results.</p>

<h1 id="extractors-in-tableview-items">Extractors in TableView Items</h1>

<p>Probably the most common use of composed <code class="language-plaintext highlighter-rouge">Property</code> objects as items in <code class="language-plaintext highlighter-rouge">ObservableLists</code> is in <code class="language-plaintext highlighter-rouge">TableView</code>.  So let’s take a look at how <code class="language-plaintext highlighter-rouge">TableView</code> columns are usually set up, and how they interact with enclosed <code class="language-plaintext highlighter-rouge">Property</code> fields.</p>

<p>Here’s the same example code, with just a <code class="language-plaintext highlighter-rouge">TableView</code> added:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">ExtractorExample5</span> <span class="p">:</span> <span class="nc">Application</span><span class="p">()</span> <span class="p">{</span>

    <span class="k">private</span> <span class="kd">val</span> <span class="py">obList</span><span class="p">:</span> <span class="nc">ObservableList</span><span class="p">&lt;</span><span class="nc">ExampleData</span><span class="p">&gt;</span> <span class="p">=</span>
        <span class="nc">FXCollections</span><span class="p">.</span><span class="nf">observableArrayList</span><span class="p">()</span>

    <span class="k">private</span> <span class="kd">val</span> <span class="py">messages</span><span class="p">:</span> <span class="nc">StringProperty</span> <span class="p">=</span> <span class="nc">SimpleStringProperty</span><span class="p">(</span><span class="s">""</span><span class="p">)</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">totBinding</span> <span class="p">=</span> <span class="kd">object</span> <span class="err">: </span><span class="nc">IntegerBinding</span><span class="p">()</span> <span class="p">{</span>
        <span class="nf">init</span> <span class="p">{</span>
            <span class="k">super</span><span class="p">.</span><span class="nf">bind</span><span class="p">(</span><span class="n">obList</span><span class="p">)</span>
        <span class="p">}</span>

        <span class="k">override</span> <span class="k">fun</span> <span class="nf">computeValue</span><span class="p">():</span> <span class="nc">Int</span> <span class="p">=</span> <span class="n">obList</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">}.</span><span class="nf">sum</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">start</span><span class="p">(</span><span class="n">stage</span><span class="p">:</span> <span class="nc">Stage</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">obList</span><span class="p">.</span><span class="nf">addListener</span><span class="p">(</span><span class="nc">InvalidationListener</span> <span class="p">{</span> <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"Invalidated \n"</span> <span class="p">})</span>
        <span class="k">for</span> <span class="p">(</span><span class="n">x</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..</span><span class="mi">5</span><span class="p">)</span> <span class="n">obList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">ExampleData</span><span class="p">(</span><span class="n">x</span><span class="p">))</span>
        <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"Data Loaded\n"</span>
        <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="n">obList</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">}.</span><span class="nf">joinToString</span><span class="p">()</span> <span class="p">+</span> <span class="s">"\n"</span>
        <span class="n">stage</span><span class="p">.</span><span class="n">scene</span> <span class="p">=</span> <span class="nc">Scene</span><span class="p">(</span><span class="nf">createContent</span><span class="p">()).</span><span class="nf">apply</span> <span class="p">{</span> <span class="p">}</span>
        <span class="n">stage</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">createContent</span><span class="p">():</span> <span class="nc">Region</span> <span class="p">=</span> <span class="nc">BorderPane</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="n">top</span> <span class="p">=</span> <span class="nc">HBox</span><span class="p">(</span>
            <span class="mf">10.0</span><span class="p">,</span>
            <span class="nc">Label</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span> <span class="nf">textProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="n">totBinding</span><span class="p">.</span><span class="nf">asString</span><span class="p">())</span> <span class="p">},</span>
            <span class="nc">Label</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span> <span class="nf">textProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="n">obList</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="n">value2</span><span class="p">.</span><span class="nf">asString</span><span class="p">())</span> <span class="p">})</span>
        <span class="n">center</span> <span class="p">=</span> <span class="nc">TextArea</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
            <span class="nf">textProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="n">messages</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="n">left</span> <span class="p">=</span> <span class="nc">TableView</span><span class="p">&lt;</span><span class="nc">ExampleData</span><span class="p">&gt;().</span><span class="nf">apply</span> <span class="p">{</span>
            <span class="n">items</span> <span class="p">=</span> <span class="n">obList</span>
            <span class="n">columns</span> <span class="p">+=</span> <span class="nc">TableColumn</span><span class="p">&lt;</span><span class="nc">ExampleData</span><span class="p">,</span> <span class="nc">Int</span><span class="p">&gt;(</span><span class="s">"Value 1"</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
                <span class="nf">setCellValueFactory</span> <span class="p">{</span> <span class="n">p</span> <span class="p">-&gt;</span> <span class="n">p</span><span class="p">.</span><span class="n">value</span><span class="p">.</span><span class="n">value1</span><span class="p">.</span><span class="nf">asObject</span><span class="p">()</span> <span class="p">}</span>
            <span class="p">}</span>
            <span class="n">columns</span> <span class="p">+=</span> <span class="nc">TableColumn</span><span class="p">&lt;</span><span class="nc">ExampleData</span><span class="p">,</span> <span class="nc">Int</span><span class="p">&gt;(</span><span class="s">"Value 2"</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
                <span class="n">maxHeight</span> <span class="p">=</span> <span class="mf">100.0</span>
                <span class="nf">setCellValueFactory</span> <span class="p">{</span> <span class="n">p</span> <span class="p">-&gt;</span> <span class="n">p</span><span class="p">.</span><span class="n">value</span><span class="p">.</span><span class="n">value2</span><span class="p">.</span><span class="nf">asObject</span><span class="p">()</span> <span class="p">}</span>
                <span class="nf">setCellFactory</span> <span class="p">{</span> <span class="n">column</span> <span class="p">-&gt;</span>
                    <span class="kd">object</span> <span class="err">: </span><span class="nc">TableCell</span><span class="p">&lt;</span><span class="nc">ExampleData</span><span class="p">,</span> <span class="nc">Int</span><span class="p">&gt;()</span> <span class="p">{</span>
                        <span class="k">override</span> <span class="k">fun</span> <span class="nf">updateItem</span><span class="p">(</span><span class="n">newItem</span><span class="p">:</span> <span class="nc">Int</span><span class="p">?,</span> <span class="n">empty</span><span class="p">:</span> <span class="nc">Boolean</span><span class="p">)</span> <span class="p">{</span>
                            <span class="k">super</span><span class="p">.</span><span class="nf">updateItem</span><span class="p">(</span><span class="n">newItem</span><span class="p">,</span> <span class="n">empty</span><span class="p">)</span>
                            <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"Table Cell Update: $newItem\n"</span>
                            <span class="n">text</span> <span class="p">=</span> <span class="k">null</span>
                            <span class="n">graphic</span> <span class="p">=</span> <span class="k">null</span>
                            <span class="n">newItem</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span>
                                <span class="k">if</span> <span class="p">(!</span><span class="n">empty</span><span class="p">)</span> <span class="p">{</span>
                                    <span class="n">text</span> <span class="p">=</span> <span class="n">newItem</span><span class="p">.</span><span class="nf">toString</span><span class="p">()</span>
                                <span class="p">}</span>
                            <span class="p">}</span>
                        <span class="p">}</span>
                    <span class="p">}</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="n">bottom</span> <span class="p">=</span> <span class="nc">Button</span><span class="p">(</span><span class="s">"Increment Item 2"</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
            <span class="nf">setOnAction</span> <span class="p">{</span>
                <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"Button 1 Clicked\n"</span>
                <span class="nf">with</span><span class="p">(</span><span class="n">obList</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span> <span class="p">{</span> <span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">+</span> <span class="mi">1</span> <span class="p">}</span>
                <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="n">obList</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">value2</span><span class="p">.</span><span class="n">value</span> <span class="p">}.</span><span class="nf">joinToString</span><span class="p">()</span> <span class="p">+</span> <span class="s">"\n"</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="n">padding</span> <span class="p">=</span> <span class="nc">Insets</span><span class="p">(</span><span class="mf">20.0</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>


<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nc">Application</span><span class="p">.</span><span class="nf">launch</span><span class="p">(</span><span class="nc">ExtractorExample5</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">)</span>
</code></pre></div></div>
<p>This is very close to the first example, but with the <code class="language-plaintext highlighter-rouge">TableView</code> added.  I’ve also removed the extractor from the <code class="language-plaintext highlighter-rouge">ObservableList</code>, so what we will see is pure <code class="language-plaintext highlighter-rouge">TableView</code> behaviour.</p>

<p>I added a custom <code class="language-plaintext highlighter-rouge">TableCell</code> to the second column so that we could see when the <code class="language-plaintext highlighter-rouge">updateItem()</code> method gets called, and what value it receives.  I’ve also limited the height of the <code class="language-plaintext highlighter-rouge">TableView</code> to restrict the number of <code class="language-plaintext highlighter-rouge">TableCells</code> instantiated because they generate a lot of messages, even if empty:</p>

<p><img src="/assets/elements/Extractors9.png" alt="Screen Snap 9" /></p>

<p>The <code class="language-plaintext highlighter-rouge">Button</code> has been clicked twice, and you can see from the <code class="language-plaintext highlighter-rouge">TableView</code> that it has updated correctly.  You can see from the messages, that the <code class="language-plaintext highlighter-rouge">TableCells</code> get updated a lot just setting things up.  If you look at the bottom of the messages you can see the two <code class="language-plaintext highlighter-rouge">Button</code> clicks, and you can see the <code class="language-plaintext highlighter-rouge">TableCell</code> updating right away.  You do not see any invalidation messages from the <code class="language-plaintext highlighter-rouge">ObservableList</code>, however.</p>

<p>This tells us that the <code class="language-plaintext highlighter-rouge">TableView</code> puts a <code class="language-plaintext highlighter-rouge">Listener</code> of some sort on the <code class="language-plaintext highlighter-rouge">Property</code> when it identifies it by calling the <code class="language-plaintext highlighter-rouge">setCellValueFactory</code>.  That’s the only way that this will work.</p>

<p>What happens if we put the extractor back into the <code class="language-plaintext highlighter-rouge">ObservableList</code>?  You get this:</p>

<p><img src="/assets/elements/Extractors10.png" alt="Screen Snap 10" /></p>

<p>Not much, really.</p>

<p>The first <code class="language-plaintext highlighter-rouge">Label</code> now has the correct total, as expected, and we see that the <code class="language-plaintext highlighter-rouge">ObservableList</code> invalidates after each <code class="language-plaintext highlighter-rouge">Button</code> click.  But it doesn’t cause the <code class="language-plaintext highlighter-rouge">TableCell</code> to update twice, or anything like that.</p>

<p>We can determine from this that adding an extractor to an <code class="language-plaintext highlighter-rouge">ObservableList</code> used in a <code class="language-plaintext highlighter-rouge">TableView</code> doesn’t hurt the performance of the <code class="language-plaintext highlighter-rouge">TableView</code>, but it doesn’t help it either.</p>

<p>Of course this is only true <strong>if</strong> you compose your <code class="language-plaintext highlighter-rouge">TableView</code> items from <code class="language-plaintext highlighter-rouge">Properties</code> and other <code class="language-plaintext highlighter-rouge">ObservableValues</code>.</p>

<h1 id="extractors-in-listview-items">Extractors in ListView Items</h1>

<p>So now we know that <code class="language-plaintext highlighter-rouge">TableView</code> works fine without extractors, but what about <code class="language-plaintext highlighter-rouge">ListView</code>?</p>

<p>We’ll go back to our example, and change the layout so that it shows a <code class="language-plaintext highlighter-rouge">ListView</code> instead of a <code class="language-plaintext highlighter-rouge">TableView</code>:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">fun</span> <span class="nf">createContent</span><span class="p">():</span> <span class="nc">Region</span> <span class="p">=</span> <span class="nc">BorderPane</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
    <span class="n">top</span> <span class="p">=</span> <span class="nc">HBox</span><span class="p">(</span>
        <span class="mf">10.0</span><span class="p">,</span>
        <span class="nc">Label</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span> <span class="nf">textProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="n">totBinding</span><span class="p">.</span><span class="nf">asString</span><span class="p">())</span> <span class="p">},</span>
        <span class="nc">Label</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span> <span class="nf">textProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="n">obList</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="n">value2</span><span class="p">.</span><span class="nf">asString</span><span class="p">())</span> <span class="p">})</span>
    <span class="n">center</span> <span class="p">=</span> <span class="nc">TextArea</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="nf">textProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="n">messages</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="n">left</span> <span class="p">=</span> <span class="nc">ListView</span><span class="p">&lt;</span><span class="nc">ExampleData</span><span class="p">&gt;().</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="n">items</span> <span class="p">=</span> <span class="n">obList</span>
        <span class="n">cellFactory</span> <span class="p">=</span> <span class="nc">Callback</span> <span class="p">{</span> <span class="n">_</span> <span class="p">-&gt;</span>
            <span class="kd">object</span> <span class="err">: </span><span class="nc">ListCell</span><span class="p">&lt;</span><span class="nc">ExampleData</span><span class="p">&gt;()</span> <span class="p">{</span>
                <span class="kd">val</span> <span class="py">label1</span> <span class="p">=</span> <span class="nc">Label</span><span class="p">()</span>
                <span class="kd">val</span> <span class="py">label2</span> <span class="p">=</span> <span class="nc">Label</span><span class="p">()</span>
                <span class="kd">val</span> <span class="py">layout</span> <span class="p">=</span> <span class="nc">VBox</span><span class="p">(</span><span class="mf">2.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
                    <span class="n">children</span> <span class="p">+=</span> <span class="nc">HBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">,</span> <span class="nc">Label</span><span class="p">(</span><span class="s">"Value 1:"</span><span class="p">),</span> <span class="n">label1</span><span class="p">)</span>
                    <span class="n">children</span> <span class="p">+=</span> <span class="nc">HBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">,</span> <span class="nc">Label</span><span class="p">(</span><span class="s">"Value 2:"</span><span class="p">),</span> <span class="n">label2</span><span class="p">)</span>
                    <span class="n">padding</span> <span class="p">=</span> <span class="nc">Insets</span><span class="p">(</span><span class="mf">8.0</span><span class="p">)</span>
                <span class="p">}</span>

                <span class="k">override</span> <span class="k">fun</span> <span class="nf">updateItem</span><span class="p">(</span><span class="n">newItem</span><span class="p">:</span> <span class="nc">ExampleData</span><span class="p">?,</span> <span class="n">isEmpty</span><span class="p">:</span> <span class="nc">Boolean</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">super</span><span class="p">.</span><span class="nf">updateItem</span><span class="p">(</span><span class="n">newItem</span><span class="p">,</span> <span class="n">isEmpty</span><span class="p">)</span>
                    <span class="n">graphic</span> <span class="p">=</span> <span class="k">null</span>
                    <span class="n">text</span> <span class="p">=</span> <span class="k">null</span>
                    <span class="n">newItem</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span>
                        <span class="k">if</span> <span class="p">(!</span><span class="n">isEmpty</span><span class="p">)</span> <span class="p">{</span>
                            <span class="n">label1</span><span class="p">.</span><span class="n">text</span> <span class="p">=</span> <span class="n">newItem</span><span class="p">.</span><span class="n">value1</span><span class="p">.</span><span class="n">value</span><span class="p">.</span><span class="nf">toString</span><span class="p">()</span>
                            <span class="n">label2</span><span class="p">.</span><span class="n">text</span> <span class="p">=</span> <span class="n">newItem</span><span class="p">.</span><span class="n">value2</span><span class="p">.</span><span class="n">value</span><span class="p">.</span><span class="nf">toString</span><span class="p">()</span>
                            <span class="n">graphic</span> <span class="p">=</span> <span class="n">layout</span>
                        <span class="p">}</span>
                    <span class="p">}</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="n">bottom</span> <span class="p">=</span> <span class="nc">Button</span><span class="p">(</span><span class="s">"Increment Item 2"</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="nf">setOnAction</span> <span class="p">{</span>
            <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="s">"Button 1 Clicked\n"</span>
            <span class="nf">with</span><span class="p">(</span><span class="n">obList</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span> <span class="p">{</span> <span class="n">value1</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="n">value1</span><span class="p">.</span><span class="n">value</span> <span class="p">+</span> <span class="mi">1</span> <span class="p">}</span>
            <span class="n">messages</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="n">obList</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">value1</span><span class="p">.</span><span class="n">value</span> <span class="p">}.</span><span class="nf">joinToString</span><span class="p">()</span> <span class="p">+</span> <span class="s">"\n"</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="n">padding</span> <span class="p">=</span> <span class="nc">Insets</span><span class="p">(</span><span class="mf">20.0</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Here we have a custom <code class="language-plaintext highlighter-rouge">ListCell</code> layout that contains a <code class="language-plaintext highlighter-rouge">VBox</code> holding two <code class="language-plaintext highlighter-rouge">HBoxes</code>.  Each <code class="language-plaintext highlighter-rouge">HBox</code> has two <code class="language-plaintext highlighter-rouge">Labels</code>, one is just a title, and the other is loaded with either <code class="language-plaintext highlighter-rouge">ExampleData.value1.value</code> or <code class="language-plaintext highlighter-rouge">ExampleData.value2.value</code>.  No <code class="language-plaintext highlighter-rouge">Bindings</code> are used, so there’s no reliance inside the <code class="language-plaintext highlighter-rouge">ListCell</code> on the <code class="language-plaintext highlighter-rouge">Property</code> nature of <code class="language-plaintext highlighter-rouge">value1</code> or <code class="language-plaintext highlighter-rouge">value2</code>.</p>

<p>The <code class="language-plaintext highlighter-rouge">updateItem()</code> method does the usual stuff, and loads <code class="language-plaintext highlighter-rouge">value1</code> and <code class="language-plaintext highlighter-rouge">value2</code> into their respective <code class="language-plaintext highlighter-rouge">Labels</code>.</p>

<p>The only other change to this code from the <code class="language-plaintext highlighter-rouge">TableView</code> version was to change the <code class="language-plaintext highlighter-rouge">Button</code> action to update <code class="language-plaintext highlighter-rouge">ExampleData.value1</code> instead of <code class="language-plaintext highlighter-rouge">ExampleData.value2</code>.  The <code class="language-plaintext highlighter-rouge">ObservableList</code> still has an extractor that returns <code class="language-plaintext highlighter-rouge">value2</code>, so this change essentially disables the extractor for this example.</p>

<p>This is what it looks like when it is run and the <code class="language-plaintext highlighter-rouge">Button</code> is clicked a few times:</p>

<p><img src="/assets/elements/Extractors11.png" alt="Screen Snap" /></p>

<p>You can see that the values displayed in the <code class="language-plaintext highlighter-rouge">ListView</code> do not change at all.  You can see in the <code class="language-plaintext highlighter-rouge">TextArea</code> that the <code class="language-plaintext highlighter-rouge">Button</code> has been clicked several times, yet the <code class="language-plaintext highlighter-rouge">ObservableList</code> never invalidated, but that <code class="language-plaintext highlighter-rouge">value1</code> did increment each time.</p>

<p>If we change the extractor to return <code class="language-plaintext highlighter-rouge">value1</code> instead of <code class="language-plaintext highlighter-rouge">value2</code>, this is what it looks like:</p>

<p><img src="/assets/elements/Extractors12.png" alt="Screen Snap" /></p>

<p>Now you can see that the <code class="language-plaintext highlighter-rouge">ListView</code> values are correct, and that the <code class="language-plaintext highlighter-rouge">Button</code> click causes the <code class="language-plaintext highlighter-rouge">ObservableList</code> to invalidate.</p>

<h2 id="is-this-a-good-approach">Is This a Good Approach?</h2>

<p>This approach has the advantage that you can implement a very naive design for the <code class="language-plaintext highlighter-rouge">ListCell</code>, without having to worry about binding and unbinding from a changing <code class="language-plaintext highlighter-rouge">itemProperty()</code>.  This is pretty much exactly the same approach to <code class="language-plaintext highlighter-rouge">ListCell</code> design that you’ll see as the copypasta example in virtually every online tutorial about <code class="language-plaintext highlighter-rouge">ListView</code>.  It’s simple, and it’s easy, and it’s what everybody knows.</p>

<p>However, it effectively splits the logic involved in keeping the <code class="language-plaintext highlighter-rouge">ListCell</code> up to date between two places, the <code class="language-plaintext highlighter-rouge">ListCell</code> itself, and the extractor.</p>

<div class="notice--primary">
 <img src="/assets/logos/brain.png" alt="primary" style="float:left;margin-right: 10px" />
 <p style="overflow:auto; float:none">
   Always ask yourself this question: "How much code will a maintenance programmer need to look at to fix a bug with this feature?"
 </p>
</div>

<p>In this case, you cannot really understand how the <code class="language-plaintext highlighter-rouge">ListCell</code> works without looking at the instantiation of <code class="language-plaintext highlighter-rouge">ObservableList</code> to see that it has an extractor.  That’s very likely to be in another class altogether.</p>

<p>Some time back, before JavaFX 19, achieving this entirely within the <code class="language-plaintext highlighter-rouge">ListCell</code> would have been a bit tedious.  The <code class="language-plaintext highlighter-rouge">Labels</code> would need to be bound to fields inside a value that itself was changing.  This would have required unbinding and the rebinding the <code class="language-plaintext highlighter-rouge">Labels</code> each time the value in the <code class="language-plaintext highlighter-rouge">ListCell</code> was changed.</p>

<p>But JavaFX 19 introduced <code class="language-plaintext highlighter-rouge">ObservableValue.flatmap()</code>, and now we can skip all the unbinding and rebinding.</p>

<p>Here’s the <code class="language-plaintext highlighter-rouge">ListCell</code> implemented that way:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cellFactory</span> <span class="p">=</span> <span class="nc">Callback</span> <span class="p">{</span> <span class="n">_</span> <span class="p">-&gt;</span>
    <span class="kd">object</span> <span class="err">: </span><span class="nc">ListCell</span><span class="p">&lt;</span><span class="nc">ExampleData</span><span class="p">&gt;()</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">label1</span> <span class="p">=</span> <span class="nc">Label</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
            <span class="nf">textProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="nf">itemProperty</span><span class="p">().</span><span class="nf">flatMap</span> <span class="p">{</span> <span class="n">item</span> <span class="p">-&gt;</span> <span class="n">item</span><span class="p">.</span><span class="n">value1</span><span class="p">.</span><span class="nf">asString</span><span class="p">()</span> <span class="p">})</span>
        <span class="p">}</span>
        <span class="kd">val</span> <span class="py">label2</span> <span class="p">=</span> <span class="nc">Label</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
            <span class="nf">textProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="nf">itemProperty</span><span class="p">().</span><span class="nf">flatMap</span> <span class="p">{</span> <span class="n">item</span> <span class="p">-&gt;</span> <span class="n">item</span><span class="p">.</span><span class="n">value2</span><span class="p">.</span><span class="nf">asString</span><span class="p">()</span> <span class="p">})</span>
        <span class="p">}</span>
        <span class="kd">val</span> <span class="py">layout</span> <span class="p">=</span> <span class="nc">VBox</span><span class="p">(</span><span class="mf">2.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
            <span class="n">children</span> <span class="p">+=</span> <span class="nc">HBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">,</span> <span class="nc">Label</span><span class="p">(</span><span class="s">"Value 1:"</span><span class="p">),</span> <span class="n">label1</span><span class="p">)</span>
            <span class="n">children</span> <span class="p">+=</span> <span class="nc">HBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">,</span> <span class="nc">Label</span><span class="p">(</span><span class="s">"Value 2:"</span><span class="p">),</span> <span class="n">label2</span><span class="p">)</span>
            <span class="n">padding</span> <span class="p">=</span> <span class="nc">Insets</span><span class="p">(</span><span class="mf">8.0</span><span class="p">)</span>
        <span class="p">}</span>

        <span class="nf">init</span> <span class="p">{</span>
            <span class="nf">graphicProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span>
                <span class="nc">Bindings</span><span class="p">.</span><span class="n">createObjectBinding</span><span class="p">&lt;</span><span class="nc">Region</span><span class="p">&gt;(</span>
                    <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">item</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span> <span class="n">layout</span> <span class="k">else</span> <span class="k">null</span> <span class="p">},</span>
                    <span class="nf">itemProperty</span><span class="p">()</span>
                <span class="p">)</span>
            <span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">Labels</code> are bound to fields in <code class="language-plaintext highlighter-rouge">itemProperty()</code> through <code class="language-plaintext highlighter-rouge">flatMap()</code>.  Now, if the value in the field changes, or the <code class="language-plaintext highlighter-rouge">itemProperty()</code> itself changes, the binding will handle it automatically.  By also creating a <code class="language-plaintext highlighter-rouge">Binding</code> to control whether or not the <code class="language-plaintext highlighter-rouge">graphic</code> displays in the <code class="language-plaintext highlighter-rouge">init{}</code> block, we can now do away with <code class="language-plaintext highlighter-rouge">updateItem()</code> entirely.  Everything is simply dependent on <code class="language-plaintext highlighter-rouge">itemProperty()</code>.</p>

<p>In order to test this, I removed the extractor from the <code class="language-plaintext highlighter-rouge">ObservableList</code>.  The result looks like this:</p>

<p><img src="/assets/elements/Extractors13.png" alt="Screen Snap" /></p>

<p>You can see that the items in the <code class="language-plaintext highlighter-rouge">ListView</code> are still correct, and the messages show the <code class="language-plaintext highlighter-rouge">Button</code> clicks that do <strong>not</strong> trigger an “Invalidated” message from the <code class="language-plaintext highlighter-rouge">Listener</code>.</p>

<p>Obviously, this requires a somewhat deeper understanding of the <code class="language-plaintext highlighter-rouge">ListCell</code> mechanics.  Most beginners aren’t even aware that <code class="language-plaintext highlighter-rouge">item</code> exists in the <code class="language-plaintext highlighter-rouge">ListCell</code> as on <code class="language-plaintext highlighter-rouge">ObjectProperty</code>, and they assume that the only way to deal with a changing <code class="language-plaintext highlighter-rouge">item</code> is through <code class="language-plaintext highlighter-rouge">updateItem()</code>.</p>

<h1 id="what-about-performance">What About Performance?</h1>

<p>You may be thinking, “What if I have an <code class="language-plaintext highlighter-rouge">ObservableList</code> with thousands, and thousands of items?  Won’t that mean thousands and thousands of <code class="language-plaintext highlighter-rouge">Listeners</code> running all the time?”</p>

<p>For sure, there is some overhead to <code class="language-plaintext highlighter-rouge">Listeners</code>, but it’s not as much as you might think.  Let’s look at what happens when you add an <code class="language-plaintext highlighter-rouge">InvalidationListener</code> to an <code class="language-plaintext highlighter-rouge">Observable</code>…</p>

<p>To find this you need to look at <code class="language-plaintext highlighter-rouge">ObjectPropertyBase</code> which is the highest abstract class that implements <code class="language-plaintext highlighter-rouge">addListener()</code> for <code class="language-plaintext highlighter-rouge">Properties</code>.  In it, you’ll find that it delegates to something called <code class="language-plaintext highlighter-rouge">ExpressionHelper</code> which is an instance variable of <code class="language-plaintext highlighter-rouge">ObjectPropertyBase</code>, and then that ends up in a subclass of <code class="language-plaintext highlighter-rouge">ExpressionHelper</code> called <code class="language-plaintext highlighter-rouge">Generic</code>.  In there, you will find this:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">protected</span> <span class="nc">Generic</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="nf">addListener</span><span class="o">(</span><span class="nc">InvalidationListener</span> <span class="n">listener</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">invalidationListeners</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">invalidationListeners</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">InvalidationListener</span><span class="o">[]</span> <span class="o">{</span><span class="n">listener</span><span class="o">};</span>
        <span class="n">invalidationSize</span> <span class="o">=</span> <span class="mi">1</span><span class="o">;</span>
    <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
        <span class="kd">final</span> <span class="kt">int</span> <span class="n">oldCapacity</span> <span class="o">=</span> <span class="n">invalidationListeners</span><span class="o">.</span><span class="na">length</span><span class="o">;</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">locked</span><span class="o">)</span> <span class="o">{</span>
            <span class="kd">final</span> <span class="kt">int</span> <span class="n">newCapacity</span> <span class="o">=</span> <span class="o">(</span><span class="n">invalidationSize</span> <span class="o">&lt;</span> <span class="n">oldCapacity</span><span class="o">)?</span> <span class="n">oldCapacity</span> <span class="o">:</span> <span class="o">(</span><span class="n">oldCapacity</span> <span class="o">*</span> <span class="mi">3</span><span class="o">)/</span><span class="mi">2</span> <span class="o">+</span> <span class="mi">1</span><span class="o">;</span>
            <span class="n">invalidationListeners</span> <span class="o">=</span> <span class="nc">Arrays</span><span class="o">.</span><span class="na">copyOf</span><span class="o">(</span><span class="n">invalidationListeners</span><span class="o">,</span> <span class="n">newCapacity</span><span class="o">);</span>
        <span class="o">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">(</span><span class="n">invalidationSize</span> <span class="o">==</span> <span class="n">oldCapacity</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">invalidationSize</span> <span class="o">=</span> <span class="n">trim</span><span class="o">(</span><span class="n">invalidationSize</span><span class="o">,</span> <span class="n">invalidationListeners</span><span class="o">);</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">invalidationSize</span> <span class="o">==</span> <span class="n">oldCapacity</span><span class="o">)</span> <span class="o">{</span>
                <span class="kd">final</span> <span class="kt">int</span> <span class="n">newCapacity</span> <span class="o">=</span> <span class="o">(</span><span class="n">oldCapacity</span> <span class="o">*</span> <span class="mi">3</span><span class="o">)/</span><span class="mi">2</span> <span class="o">+</span> <span class="mi">1</span><span class="o">;</span>
                <span class="n">invalidationListeners</span> <span class="o">=</span> <span class="nc">Arrays</span><span class="o">.</span><span class="na">copyOf</span><span class="o">(</span><span class="n">invalidationListeners</span><span class="o">,</span> <span class="n">newCapacity</span><span class="o">);</span>
            <span class="o">}</span>
        <span class="o">}</span>
        <span class="n">invalidationListeners</span><span class="o">[</span><span class="n">invalidationSize</span><span class="o">++]</span> <span class="o">=</span> <span class="n">listener</span><span class="o">;</span>
    <span class="o">}</span>
    <span class="k">return</span> <span class="k">this</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Yikes!</p>

<p>In summary, it has an array of <code class="language-plaintext highlighter-rouge">InvalidationListeners</code> and it adds your new <code class="language-plaintext highlighter-rouge">InvalidationListener</code> to it.  The remaining dozen or so lines of appear to be some kind of memory management optimization.  That’s good to see, if you are worried about performance degredation associated with extractors.</p>

<p>So, this means that every <code class="language-plaintext highlighter-rouge">Observable</code> potentially contains an <code class="language-plaintext highlighter-rouge">Array</code> that holds all of the <code class="language-plaintext highlighter-rouge">InvalidationListeners</code> that have been added to it.</p>

<p>How does it use this?</p>

<p>In <code class="language-plaintext highlighter-rouge">ObjectPropertyBase</code> we have this:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">set</span><span class="o">(</span><span class="no">T</span> <span class="n">newValue</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">isBound</span><span class="o">())</span> <span class="o">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="n">java</span><span class="o">.</span><span class="na">lang</span><span class="o">.</span><span class="na">RuntimeException</span><span class="o">((</span><span class="n">getBean</span><span class="o">()</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">getName</span><span class="o">()</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">?</span>
                <span class="n">getBean</span><span class="o">().</span><span class="na">getClass</span><span class="o">().</span><span class="na">getSimpleName</span><span class="o">()</span> <span class="o">+</span> <span class="s">"."</span> <span class="o">+</span> <span class="n">getName</span><span class="o">()</span> <span class="o">+</span> <span class="s">" : "</span><span class="o">:</span> <span class="s">""</span><span class="o">)</span> <span class="o">+</span> <span class="s">"A bound value cannot be set."</span><span class="o">);</span>
    <span class="o">}</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">value</span> <span class="o">!=</span> <span class="n">newValue</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">value</span> <span class="o">=</span> <span class="n">newValue</span><span class="o">;</span>
        <span class="n">markInvalid</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>The most important part for us is the call to <code class="language-plaintext highlighter-rouge">markInvalid()</code>:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">protected</span> <span class="kt">void</span> <span class="nf">fireValueChangedEvent</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">ExpressionHelper</span><span class="o">.</span><span class="na">fireValueChangedEvent</span><span class="o">(</span><span class="n">helper</span><span class="o">);</span>
<span class="o">}</span>

<span class="kd">private</span> <span class="kt">void</span> <span class="nf">markInvalid</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">valid</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">valid</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span>
        <span class="n">invalidated</span><span class="o">();</span>
        <span class="n">fireValueChangedEvent</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>And we are back to <code class="language-plaintext highlighter-rouge">ExpressionHelper</code> again, and then back into <code class="language-plaintext highlighter-rouge">Generic</code> for the actual implementation:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">curInvalidationSize</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="n">curInvalidationList</span><span class="o">[</span><span class="n">i</span><span class="o">].</span><span class="na">invalidated</span><span class="o">(</span><span class="n">observable</span><span class="o">);</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Thread</span><span class="o">.</span><span class="na">currentThread</span><span class="o">().</span><span class="na">getUncaughtExceptionHandler</span><span class="o">().</span><span class="na">uncaughtException</span><span class="o">(</span><span class="nc">Thread</span><span class="o">.</span><span class="na">currentThread</span><span class="o">(),</span> <span class="n">e</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>This is just a snippet of the code for this method, but you can see how it works.  It loops through the <code class="language-plaintext highlighter-rouge">Array</code> of <code class="language-plaintext highlighter-rouge">InvalidationListeners</code> and invokes <code class="language-plaintext highlighter-rouge">invalidated()</code> on each one.  Remember that <code class="language-plaintext highlighter-rouge">InvalidationListener</code> is a Functional Interface with one method, <code class="language-plaintext highlighter-rouge">invalidated()</code>.</p>

<p>If you were picturing an implementation where JavaFX had some master list of <code class="language-plaintext highlighter-rouge">Listeners</code> and it had to check them all every time something changed, then you can see that this is backwards to that.  Each <code class="language-plaintext highlighter-rouge">Observable</code> maintains its own list of <code class="language-plaintext highlighter-rouge">Listeners</code> that have been registered with it.  Then when its value is updated, it triggers each of those <code class="language-plaintext highlighter-rouge">InvalidationListeners</code>.</p>

<p>If there is a potential performance hit, it would be from having many <code class="language-plaintext highlighter-rouge">Listeners</code> on each <code class="language-plaintext highlighter-rouge">Observable</code> item.  Not from having many <code class="language-plaintext highlighter-rouge">Observable</code> items with one or two <code class="language-plaintext highlighter-rouge">Listeners</code> each.</p>

<h1 id="conclusion">Conclusion</h1>

<p>Creating an <code class="language-plaintext highlighter-rouge">ObservableList</code> with an extractor causes just two things to happen:</p>

<ol>
  <li>The <code class="language-plaintext highlighter-rouge">ObservableList</code> will invalidate, and fire any <code class="language-plaintext highlighter-rouge">InvalidationListeners</code> on it when any <code class="language-plaintext highlighter-rouge">Observable</code> returned from the extractor invalidates.</li>
  <li>If you wrap the <code class="language-plaintext highlighter-rouge">ObservableList</code> in a <code class="language-plaintext highlighter-rouge">ListProperty</code> that <code class="language-plaintext highlighter-rouge">ListProperty</code> will both invalidate and fire and <code class="language-plaintext highlighter-rouge">ChangeListeners</code> on it when any <code class="language-plaintext highlighter-rouge">Observable</code> return from the extractor invalidates.</li>
</ol>

<p>Invalidation is <strong>the</strong> key concept on <code class="language-plaintext highlighter-rouge">Bindings</code>.  This is because <code class="language-plaintext highlighter-rouge">Bindings</code> always recalculate by calling their <code class="language-plaintext highlighter-rouge">computeValue()</code> method when any of their dependencies invalidates.</p>

<p>This means that you can create <code class="language-plaintext highlighter-rouge">Bindings</code> on <code class="language-plaintext highlighter-rouge">ObservableLists</code> that will re-evaluate not just when items in the <code class="language-plaintext highlighter-rouge">ObservableList</code> are added, removed or replaced, but when internal values in those <code class="language-plaintext highlighter-rouge">ObservableList</code> items change…if you add the appropriate extractor.</p>]]></content><author><name>Dave Barrett</name><email>pragmaticcoding8@gmail.com</email></author><category term="javafx" /><summary type="html"><![CDATA[ObservableList extractors allow you to track changes to elements inside your ObservableLists.]]></summary></entry><entry><title type="html">Converting FXML to Code</title><link href="https://www.pragmaticcoding.ca/javafx/elements/fxml-to-code" rel="alternate" type="text/html" title="Converting FXML to Code" /><published>2025-07-22T17:00:00+00:00</published><updated>2025-07-22T17:00:00+00:00</updated><id>https://www.pragmaticcoding.ca/javafx/elements/fxml-to-code</id><content type="html" xml:base="https://www.pragmaticcoding.ca/javafx/elements/fxml-to-code"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>Recently, I posted an article titled, <a href="/javafx/fxml-or-not">Should You Use FXML?</a>.  In that article, I stated that I felt that well written and organized code would always be easier to maintain than any corresponding FXML/FXML Controller would be.  I did not emphasize this, as the article was intended to be a discussion about the merits and costs of using FXML more than as a “is this better than this?”, exploration.</p>

<p>In the article, I included a sample of a large (462 line) FXML file that I grabbed somewhat randomly from GitHub.  Some readers expressed the opinion that the FXML was “bad”, and that they write better FXML by hand.  Some expressed concern that I had cherry-picked bad FXML to use as an example, while others seemed upset at the idea of randomly picking FXML as an example.</p>

<p>I wasn’t sure about how to respond to these comments because I realized that the frame of reference between myself and these commentors was just too different.  It really didn’t matter to me if the FXML example that I picked was good, bad or average.</p>

<p>It didn’t matter because I understand that the way that I write layout code is astronomically better than FXML can ever be.</p>

<p>But I couldn’t say that.  Who would believe me?  What makes me think my coding is so much better?</p>

<p>This article is my attempt to explain my position.  I’m going to take that example FXML and its FXML Controller code (and other stuff it turns out it needs to work), and re-write it as purely coded layout in Kotlin.</p>

<p>My hope is that you can look at the original FXML and code, and then look at my version, and you will see the potential for writing your layouts by hand.</p>

<p>And this is not about <em>me</em> or <em>my</em> coding skill.  There’s nothing in my version that anyone reading this article couldn’t do themselves, or learn to do themselves.</p>

<h1 id="approach">Approach</h1>

<p>Generally speaking, the goal is to reproduce the layout as close as possible to the original, so that it becomes clear just how much easier it is to understand and maintain a hand coded layout.  However, there are secondary considerations that need to be taken into account:</p>

<dl>
  <dt>Improving the Layout</dt>
  <dd>
    <p>I was torn about this at first.  Eventually, I decided that there might be aspects of layout design that are adversely affected by using SceneBuilder to create the FXML file.  It might be obvious, when hand-coding, that the same look and feel can be achieved through a better design.  I have chosen to implement these improvements as these are generally problems that wouldn’t arise coding the layout by hand.</p>
  </dd>
</dl>

<div class="notice--primary">
 <img src="/assets/logos/brain.png" alt="primary" style="float:left;margin-right: 10px" />
 <p style="overflow:auto; float:none">
   This shows that the impact of hand-coding goes beyond being simply a matter of clarity and maintainability, but also affects the application design.
 </p>
</div>

<dl>
  <dt>Implementing a Framework</dt>
  <dd>
    <p>The orginal design, like most FXML implementations, has too much functionality in the FXML Controller.  This is probably through the misplaced belief that the FXML Controller acts as an MVC Controller.  In order to understand how a hand-coded layout is better in this respect, a MVCI framework has been applied.  This results in the creation of a Presentation Model, and moving all of the application logic found in the FXML Controller into the Interactor.</p>
  </dd>
  <dt>Changing to a Reactive Design</dt>
  <dd>
    <p>It’s clear to me that JavaFX is intended to be used as a Reactive framework, and that it just works better that way.  Systems built with a Reactive approach are simpler and cleaner, and easier to understand.</p>
  </dd>
  <dt>Threading and the FXAT</dt>
  <dd>
    <p>The FXML Controller doesn’t have any code that attempts to perform potentially blocking (like file access) operations off the FXAT.  There’s no point in creating bad code for conversion, so I’ve organized the code such that thread handling is performed as it should be.</p>
  </dd>
  <dt>WidgetsFX</dt>
  <dd>
    <p>Since this is intended as an example of how a real application would be put together, I have chosen to use my own WidgetsFX library to implement many of the builders, extension functions and helper classes that I would ordinarily use to build layouts.  I’m not including any of that code here, but you can easily tell what it would do.</p>
  </dd>
</dl>

<p class="notice--primary">I also freely admit that I worked with the WidgetsFX project open, and that I added new functionality to it as required.  This is the way that I would ordinarily work, and it’s also how the WidgetsFX library grows organically over time.</p>

<dl>
  <dt>Styling</dt>
  <dd>
    <p>I’ve chosen to move any styling in the FXML or the FXML Controller into an external style sheet.  Then I’m not going to create that style sheet because…why bother.  The result is that the screens in my project, while having an identical effective layout, don’t look like the originals…but that’s not the point.</p>
  </dd>
</dl>

<h2 id="kotlin">Kotlin</h2>

<p>I’m writing this all in Kotlin for two reasons.  Firstly, I find Java painful and unsatisfying to code with after several years of writing mostly Kotlin.  Secondly, Kotlin just makes it so much easier to write clear, easy to understand layout code.</p>

<p>I think that, even if you don’t fully understand the syntax, the Kotlin code is easy enough to understand for most Java programmers.  Take a look at this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">VBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
  <span class="n">padding</span> <span class="p">=</span> <span class="nc">Insets</span><span class="p">(</span><span class="mf">25.0</span><span class="p">)</span>
    <span class="p">.</span>
    <span class="p">.</span>
    <span class="p">.</span>
  <span class="n">children</span> <span class="p">+=</span> <span class="nf">intSpinnerOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">50</span><span class="p">,</span> <span class="s">"standard-spinner"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">connectorThickness</span><span class="p">.</span><span class="nf">asObject</span><span class="p">())</span>
        <span class="p">.</span><span class="nf">setStep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">bindDisable</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">selectedDistance</span><span class="p">.</span><span class="n">isNull</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">withInitialDelay</span><span class="p">(</span><span class="nc">Duration</span><span class="p">.</span><span class="nf">millis</span><span class="p">(</span><span class="mf">500.0</span><span class="p">))</span>
        <span class="p">.</span><span class="nf">withRepeatDelay</span><span class="p">(</span><span class="nc">Duration</span><span class="p">.</span><span class="nf">millis</span><span class="p">(</span><span class="mf">500.0</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>You don’t really need to know the details of how the extension function <code class="language-plaintext highlighter-rouge">.apply{}</code> works to understand that the code inside the <code class="language-plaintext highlighter-rouge">{}</code> configures the <code class="language-plaintext highlighter-rouge">VBox</code>.  You can see that the padding is set to 25px, even though you don’t understand that Kotlin allows you to refer directly to values of fields that have <code class="language-plaintext highlighter-rouge">getters</code> and <code class="language-plaintext highlighter-rouge">setters</code> and that it will still call those <code class="language-plaintext highlighter-rouge">getters</code> and <code class="language-plaintext highlighter-rouge">setters</code>.  Then, <code class="language-plaintext highlighter-rouge">children += </code> is clearly using the <code class="language-plaintext highlighter-rouge">+=</code> operator on a <code class="language-plaintext highlighter-rouge">List</code> to do the equivalent of <code class="language-plaintext highlighter-rouge">getChildren().add()</code>.</p>

<p>It’s also clear that <code class="language-plaintext highlighter-rouge">setStep()</code>, <code class="language-plaintext highlighter-rouge">bindDisable()</code>, <code class="language-plaintext highlighter-rouge">withInitialDelay()</code> and <code class="language-plaintext highlighter-rouge">withRepeatDelay()</code> are all configuration methods for the <code class="language-plaintext highlighter-rouge">Spinner</code>.  Since they are chained, all of these methods are designed as decorators.</p>

<p>It’s obviously not clear what the parameters of <code class="language-plaintext highlighter-rouge">intSpinnerOf()</code> are, but Intellij displays what the parameter names are.  So when you really are working with this code, there is no question what the parameters mean.</p>

<p>For purposes of this article, pretty much any Java programmer can look at this snippet of code and understand that it defines a <code class="language-plaintext highlighter-rouge">VBox</code> with a particular padding and that it contains a <code class="language-plaintext highlighter-rouge">Spinner&lt;Integer&gt;</code> that has been configured in a particular way and bound to some <code class="language-plaintext highlighter-rouge">Property</code> in the Model.  That’s probably enough to get the point.</p>

<h1 id="the-original-version">The Original Version</h1>

<p>You can find this project on GitHub <a href="https://github.com/trinity-xai/Trinity">here</a>.</p>

<p>I was concerned that the project might be deleted or change beyond recognition over time, so I placed a snapshot of the FXML file, the FXML Controller and a few supporting files in this <a href="/pages/trinity-source">article</a>.  I’m not going to include all the code in this article itself, because it’s just going to be too big.</p>

<p>When the program is running, the screen looks like this, although this version from the read.me page appears to be out of date and doesn’t quite match what the code does:</p>

<p><img src="/assets/images/TrinityScreenShot.png" alt="Trinity Screenshot" /></p>

<p>The code we are working on is the control panel in the upper left corner, so here it is close-up:</p>

<p><img src="/assets/images/TrinityScreenShot1.png" alt="Trinty Control Panel" /></p>

<p>Unfortunately, we don’t have screen shots of all of the <code class="language-plaintext highlighter-rouge">Tabs</code> in the control panel, but you can get a sense of the design from this.</p>

<p>There’s obviously some pretty strong styling going on here, it looks like maybe they used AtlantaFX?  I didn’t go looking, and I’m not going to make any attempt to duplicate it.  There is zero code or FXML that applies any of this styling, so I’m deeming it “out of scope”.</p>

<h1 id="notes-while-performing-the-conversion">Notes While Performing the Conversion</h1>

<p>As I went through the process of performing the conversion, I made a point of taking some notes about issues that I encountered and ideas that occured to me…</p>

<h2 id="this-was-probably-generated-from-scenebuilder">This Was Probably Generated From SceneBuilder</h2>

<p>I came across this in the FXML file:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;HBox</span> <span class="na">alignment=</span><span class="s">"CENTER"</span> <span class="na">spacing=</span><span class="s">"10.0"</span> <span class="na">GridPane.columnSpan=</span><span class="s">"2147483647"</span> <span class="na">GridPane.rowIndex=</span><span class="s">"8"</span><span class="nt">&gt;</span>
</code></pre></div></div>
<p>I cannot see any human entering “2147483647” as a <code class="language-plaintext highlighter-rouge">columnSpan</code> value.  So this is either generated from SceneBuilder, or copypasta from some section that <em>was</em> generated by SceneBuilder.</p>

<p>Why does this matter?  If the contention is that FXML is clear and easy to read, and yet we can blame most of the strange structures that make this file difficult to understand on SceneBuilder - then that’s an issue.</p>

<h2 id="the-design-is-difficult-to-understand">The Design is Difficult to Understand</h2>

<p>It’s monolithic.  462 lines of FXML, and you need to scan all of it to understand that it’s basically just 4 <code class="language-plaintext highlighter-rouge">Tabs</code> in a <code class="language-plaintext highlighter-rouge">TabPane</code>.  The <code class="language-plaintext highlighter-rouge">GridPanes</code> were a particular chore, as the components were not organized in the FXML file at all, just jumbled up willy-nilly and difficult to locate.</p>

<div class="notice--primary">
 <img src="/assets/logos/brain.png" alt="primary" style="float:left;margin-right: 10px" />
 <p style="overflow:auto; float:none">
   All in all, I spent more time trying to understand the FXML and FXML Controller than I did writing my own version.
 </p>
</div>

<h2 id="bad-fxml-doesnt-appear-be-more-verbose">“Bad” FXML Doesn’t Appear Be More Verbose</h2>

<p>It has become clear that you wouldn’t write FXML like this by hand.  It’s just too jumbled up for that, and probably crosses into the realm of “bad” FXML.  However, there really isn’t anything in its jumbled-upness that makes it longer or more verbose.  As a matter of fact, there are things about it - like the lack of “row” or “column” specifications for <code class="language-plaintext highlighter-rouge">GridPane</code> elements in row or column 0 - that make it a bit less verbose.</p>

<p>The inescapable truth seems to be that if you are going to create a layout of this complexity, then you are going to need 450+ lines of FXML to do it.  No matter how good your FXML writing skills are.</p>

<h2 id="dealing-with-filechooser">Dealing With FileChooser</h2>

<p>In most of my programming, I don’t use <code class="language-plaintext highlighter-rouge">FileChooser</code> much at all, but this project and this screen do.  So I had to grapple with the question of “Where does it go in the MVCI structure?”.  It can be argued that <code class="language-plaintext highlighter-rouge">FileChooser</code> is a GUI element, and therefore belongs to the View, and that’s were I initially placed it.  But then I ended up with references to <code class="language-plaintext highlighter-rouge">File</code> objects, and that caused me to look closer at this approach.</p>

<p>First off, <code class="language-plaintext highlighter-rouge">File</code> is most distinctly <strong>not</strong> a front end class.  It seemed worrying to have this data type handled by the GUI code.</p>

<p>Secondly, the <code class="language-plaintext highlighter-rouge">File</code> had to be passed back to the Interactor somehow, because it’s the Interactor that was going to use it.  This made it really clear to me that there was an issue.  Why does the View get to know <em>anything</em> about the back-end structure.  Furthermore, the View doesn’t actually <em>use</em> the <code class="language-plaintext highlighter-rouge">File</code> object, it just passes it back to the Controller.</p>

<p>Now, what happens if the storage is changed from JSON files to a database?  Or the application is changed to facilitate sharing designs between different users by email?  Would that mean that you would have to change the View in order to make this change?</p>

<p>For sure, that <code class="language-plaintext highlighter-rouge">FileChooser</code> would need to change.  Maybe it becomes a <code class="language-plaintext highlighter-rouge">Dialog</code> that allows for load and save to a database, or to select an email with attachments (or to send an email).  But there’s no reason that you should have to change the View for that.</p>

<p>In the end, I moved the <code class="language-plaintext highlighter-rouge">FileChooser</code> calls into the Controller, which is where I think they really belong.  It’s now integrated with the thread handling and invocation of Interactor methods.  The <code class="language-plaintext highlighter-rouge">File</code> object is no longer tramp data passing from the View through the Controller to the Interactor.</p>

<h2 id="unused-elements">Unused? Elements</h2>

<p>In the <code class="language-plaintext highlighter-rouge">Tab</code> labeled “PCA” there are two <code class="language-plaintext highlighter-rouge">RadioButtons</code>, “Use Hyperspace” and “Use Hypersurface” that do <strong>NOT</strong> have their <code class="language-plaintext highlighter-rouge">isSelected</code> values used in any code in the FXML Controller.</p>

<p>However, the <code class="language-plaintext highlighter-rouge">ToggleGroup</code> to which they belong, <code class="language-plaintext highlighter-rouge">pcahyperSourceGroup</code> is <strong>NOT</strong> private to the FXML Controller.  So it is possible that some other class with a reference to this FXML Controller is accessing this <code class="language-plaintext highlighter-rouge">ToggleGroup</code>, getting a reference to the <code class="language-plaintext highlighter-rouge">RadioButton</code> that is selected, and (Gasp!!!) checking it’s <code class="language-plaintext highlighter-rouge">text</code> value to find out which <code class="language-plaintext highlighter-rouge">RadioButton</code> it is.</p>

<p>Don’t try this at home.</p>

<p>Needless to say, I’m not sure if these <code class="language-plaintext highlighter-rouge">RadioButtons</code> do or do not do anything - and that should be immediately apparent from reading the code.  I’ve connected these <code class="language-plaintext highlighter-rouge">RadioButtons</code> to some elements in the Model that are clearly named as some kind of “dummy” value.</p>

<p>It may be that I grabbed the code in the middle of development and it just wasn’t yet complete.  There were several new releases of this project between writing and publishing this article.  On the other hand, this may just be something that somehow got lost and forgotten in the hundreds of lines of code here.</p>

<h2 id="connecting-to-the-rest-of-the-application">Connecting to the Rest of the Application</h2>

<p>When you look at the screen snaps on the project Read.me page, it’s clear that this screen is supposed to be a control/input screen for a real-time display elsewhere in the application.  As such, there needs to be a way to communicate with the rest of the application in real-time.</p>

<p>We see that the original code implements this communication through <code class="language-plaintext highlighter-rouge">EventHandlers</code>, like this:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">scene</span><span class="o">.</span><span class="na">addEventHandler</span><span class="o">(</span><span class="nc">ManifoldEvent</span><span class="o">.</span><span class="na">DISTANCE_CONNECTOR_SELECTED</span><span class="o">,</span> <span class="n">e</span> <span class="o">-&gt;</span> <span class="o">{</span>   <span class="o">}</span>
</code></pre></div></div>
<p>for incoming <code class="language-plaintext highlighter-rouge">Events</code>, and this:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">scene</span><span class="o">.</span><span class="na">getRoot</span><span class="o">().</span><span class="na">fireEvent</span><span class="o">(</span><span class="k">new</span> <span class="nc">ManifoldEvent</span><span class="o">(</span><span class="nc">ManifoldEvent</span><span class="o">.</span><span class="na">DISTANCE_CONNECTOR_WIDTH</span><span class="o">,</span> <span class="n">item</span><span class="o">.</span><span class="na">getDistance</span><span class="o">()));</span>
</code></pre></div></div>
<p>for outgoing <code class="language-plaintext highlighter-rouge">Events</code>.</p>

<p>This is probably one of the most insidious issues that can occur when you forego implementing a proper framework.  The code you are writing is <code class="language-plaintext highlighter-rouge">Event</code> centric, so then <em>everything</em> is now an <code class="language-plaintext highlighter-rouge">Event</code>.  Even when it really isn’t.  I’ll discuss this more when we look at the new design.</p>

<h1 id="changes-to-the-design">Changes to the Design</h1>

<p>It was clear when doing the conversion that there were “issues” with the original design.  Furthermore, it was clear that it would not make sense to leave these issues unaddressed when performing the conversion.</p>

<h2 id="issues-with-the-original-design">Issues With the Original Design</h2>

<p>Most of these issues, as far as I can tell, are a direct result of utilizing FXML in the way that most of the tutorials, including those from Oracle, tell you to.  From what I can see, these issues build on each other.  Let’s take a look at them:</p>

<h3 id="there-is-no-framework">There is no Framework</h3>

<p>The original implementation assumed that the FXML file was the “View” and the FXML Controller was the “Controller” and the domain objects collectively comprised the “Model”.  The results is that virtually all of the code resides in the FXML Controller and it does way, way too much while doing none of it well.</p>

<p>This is, in my opinion, a big problem with the design of this application, and this clearly shows how the automatic “separation of concerns” claimed for FXML is just a myth.  There is no separation here, you have application logic, file handling and communication with other parts of the application muddled up with the configuration of individual <code class="language-plaintext highlighter-rouge">Nodes</code>.  Within the layout itself, you have extensive coupling of <code class="language-plaintext highlighter-rouge">Nodes</code> all over the layout.</p>

<h3 id="there-is-no-presentation-model">There is no Presentation Model</h3>

<p>All of the data is stored in the screen <code class="language-plaintext highlighter-rouge">Nodes</code>, and there is no external data storage outside of the value <code class="language-plaintext highlighter-rouge">Properties</code> of the <code class="language-plaintext highlighter-rouge">Nodes</code> themselves.  This means that all of the data must to be scraped out of the <code class="language-plaintext highlighter-rouge">Nodes</code> when it’s needed, which, in turn,  means that all of the <code class="language-plaintext highlighter-rouge">Node</code> variables have to be globally scoped so that their data is available when required.  Globally scoping those variables is a massive source of coupling within the FXML Controller.</p>

<p>Of course, the design of FXML requires that all of the <code class="language-plaintext highlighter-rouge">Nodes</code> from the layout that are going to be accessed from the FXML Controller are instantiated as fields in the FXML Controller, which means that they are globally scoped.  This makes it harder to see the benefits of attempting to limit the scope of these variables by implementing a Presentation Model.</p>

<p>Finally, without a Presentation Model, it’s much more difficult to share the data with the application logic.  This is obscured in this application by including the application logic inside the FXML Controller.</p>

<h3 id="it-is-an-action-based-design">It is an “Action” Based Design</h3>

<p>Without a Presentation Model to act as a data representation of the “State” of the GUI, it is very difficult to create a Reactive design.  Instead of linking GUI elements together through the Presentation Model, they are directly referenced by other GUI elements.</p>

<p>The effect of this is that the application becames <code class="language-plaintext highlighter-rouge">Node</code>-centric, instead of data centric, and this shift, in turn, leads to an “Action” based design.</p>

<h3 id="using-events-to-communicate-between-screens">Using <code class="language-plaintext highlighter-rouge">Events</code> to Communicate Between Screens</h3>

<p>The elements in this screen are used to control the manner in which data in other windows and screens is displayed.  This means that this screen needs to communicate somehow with other windows and screens that are external to this layout.  The programmers have decided to use JavaFX <code class="language-plaintext highlighter-rouge">Events</code> to do this.  Consider this snippet of code:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">connectorThicknessSpinner</span><span class="o">.</span><span class="na">valueProperty</span><span class="o">().</span><span class="na">addListener</span><span class="o">(</span><span class="n">e</span> <span class="o">-&gt;</span> <span class="o">{</span>
    <span class="nc">DistanceListItem</span> <span class="n">item</span> <span class="o">=</span> <span class="n">distancesListView</span><span class="o">.</span><span class="na">getSelectionModel</span><span class="o">().</span><span class="na">getSelectedItem</span><span class="o">();</span>
    <span class="k">if</span> <span class="o">(</span><span class="kc">null</span> <span class="o">!=</span> <span class="n">item</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Integer</span> <span class="n">width</span> <span class="o">=</span> <span class="o">(</span><span class="nc">Integer</span><span class="o">)</span> <span class="n">connectorThicknessSpinner</span><span class="o">.</span><span class="na">getValue</span><span class="o">();</span>
        <span class="n">item</span><span class="o">.</span><span class="na">getDistance</span><span class="o">().</span><span class="na">setWidth</span><span class="o">(</span><span class="n">width</span><span class="o">);</span>
        <span class="n">scene</span><span class="o">.</span><span class="na">getRoot</span><span class="o">().</span><span class="na">fireEvent</span><span class="o">(</span>
            <span class="k">new</span> <span class="nf">ManifoldEvent</span><span class="o">(</span><span class="nc">ManifoldEvent</span><span class="o">.</span><span class="na">DISTANCE_CONNECTOR_WIDTH</span><span class="o">,</span> <span class="n">item</span><span class="o">.</span><span class="na">getDistance</span><span class="o">()));</span>
    <span class="o">}</span>
<span class="o">});</span>
</code></pre></div></div>
<p>Here we have a <code class="language-plaintext highlighter-rouge">Spinner</code> configured such that every time its value changes a <code class="language-plaintext highlighter-rouge">Listener</code> is triggered and that <code class="language-plaintext highlighter-rouge">Listener</code> will eventually fire a <code class="language-plaintext highlighter-rouge">ManifoldEvent</code> of a certain type, in this case <code class="language-plaintext highlighter-rouge">ManifoldEvent.DISTANCE_CONNECTOR_WIDTH</code> type.</p>

<p>Presumably, some external <code class="language-plaintext highlighter-rouge">EventHandler</code> is configured to respond to this <code class="language-plaintext highlighter-rouge">ManifoldEvent</code> and do something.</p>

<p>On the surface, this seems like a reasonable approach:  The programmers needed a reliable messaging facility to communicate between components, and the JavaFX <code class="language-plaintext highlighter-rouge">Event</code> system seems to fit the bill.  But there are some problems with this:</p>

<ol>
  <li>
    <p>Sequencing can be a problem.  <code class="language-plaintext highlighter-rouge">Events</code> invoke <code class="language-plaintext highlighter-rouge">EventHandlers</code> which are submitted to the FXAT.  If a system is very active, it’s possible that there’s a lot of items in the FXAT queue, and they may have changed the environment in which the <code class="language-plaintext highlighter-rouge">EventHandler</code> will run.  You cannot control this.</p>
  </li>
  <li>
    <p>It deals in domain objects.  That call to <code class="language-plaintext highlighter-rouge">item.getDistance()</code> returns a <code class="language-plaintext highlighter-rouge">Distance</code> which is a domain object.</p>
  </li>
  <li>
    <p>Related to the previous item, the domain objects are stored in singleton <code class="language-plaintext highlighter-rouge">Lists</code> in <code class="language-plaintext highlighter-rouge">Distance</code> and <code class="language-plaintext highlighter-rouge">Manifold</code>.  This leads to timing issues where domain objects might exist in the singleton lists but not in the <code class="language-plaintext highlighter-rouge">ListViews</code> yet.  There is code that defends against this and generates errors when this happens.</p>
  </li>
  <li>
    <p>Delivery is <strong>not</strong> guaranteed.  There’s nothing to say that some screen element couldn’t filter an <code class="language-plaintext highlighter-rouge">Event</code> before it gets to its intended target.  This might be hard to debug. Essentially, the <code class="language-plaintext highlighter-rouge">Event</code> system is global and couples everything that uses it.</p>
  </li>
  <li>
    <p>It’s not clear who the recipient is.  <code class="language-plaintext highlighter-rouge">Events</code> are, by definition, broadcast entities.  To find out how this <code class="language-plaintext highlighter-rouge">Event</code> is handled, you’ll have to search through the entire application to see which classes  have an <code class="language-plaintext highlighter-rouge">EventHandler</code> for the <code class="language-plaintext highlighter-rouge">ManifoldEvent.DISTANCE_CONNECTOR_WIDTH</code> subtype.</p>
  </li>
</ol>

<p>Personally, I think that <code class="language-plaintext highlighter-rouge">Events</code> and <code class="language-plaintext highlighter-rouge">EventHandlers</code> are best used for very localized things.  This would mean adding an <code class="language-plaintext highlighter-rouge">EventHandler</code> onto the <code class="language-plaintext highlighter-rouge">Button</code> that generates the <code class="language-plaintext highlighter-rouge">Event</code> (like a click <code class="language-plaintext highlighter-rouge">ActionEvent</code>).  Using the JavaFX <code class="language-plaintext highlighter-rouge">Event</code> system as a general communication bus feels like a “code smell” to me.</p>

<h3 id="the-listviews">The ListViews</h3>

<p>This is the only part of the design which is objectively “wrong” from a technical JavaFX respect.</p>

<p>There are two classes for each <code class="language-plaintext highlighter-rouge">ListView</code>, let’s look at the one for manifolds.  We have the class <code class="language-plaintext highlighter-rouge">ManifoldListItem</code> which extends <code class="language-plaintext highlighter-rouge">VBox</code>, and the class <code class="language-plaintext highlighter-rouge">Manifold</code> which we should probably consider to be a “Domain Object”.  Every <code class="language-plaintext highlighter-rouge">ManifoldListItem</code> contains a reference to a <code class="language-plaintext highlighter-rouge">Manifold</code>.</p>

<p>The biggest problem is that the <code class="language-plaintext highlighter-rouge">ListView</code> is defined as <code class="language-plaintext highlighter-rouge">ListView&lt;ManifoldListItem&gt;</code> and then has no <code class="language-plaintext highlighter-rouge">ListCell</code> defined anywhere.  This violates one of the principal rules of <code class="language-plaintext highlighter-rouge">ListView</code> and <code class="language-plaintext highlighter-rouge">TableView</code>:  Do <strong>NOT</strong> put <code class="language-plaintext highlighter-rouge">Nodes</code> as the items.</p>

<p>I don’t even know how this works.  Honestly, this was the one thing that took me the closest to actually downloading the whole project and building it to see if it actually works.</p>

<p>The <code class="language-plaintext highlighter-rouge">Manifold</code> class has a static <code class="language-plaintext highlighter-rouge">HashMap</code> field that stores all of the <code class="language-plaintext highlighter-rouge">Manifolds</code> that have been created - essentially a singleton that can be accessed through the whole application.  Whenever a new <code class="language-plaintext highlighter-rouge">Manifold</code> is created, there should also be an <code class="language-plaintext highlighter-rouge">ManifoldEvent</code> fired that should trigger an <code class="language-plaintext highlighter-rouge">EventHandler</code> added to the <code class="language-plaintext highlighter-rouge">Scene</code>.  That <code class="language-plaintext highlighter-rouge">EventHandler</code>creates a new <code class="language-plaintext highlighter-rouge">ManifoldListItem</code> and adds it to the <code class="language-plaintext highlighter-rouge">ListView</code>.</p>

<p>If the manifold <code class="language-plaintext highlighter-rouge">HashMap</code> is cleared, there should be an accompanying <code class="language-plaintext highlighter-rouge">ManifoldEvent</code> fired which trigger a different <code class="language-plaintext highlighter-rouge">EventHandler</code> set on the <code class="language-plaintext highlighter-rouge">Scene</code>.</p>

<p>There are three <code class="language-plaintext highlighter-rouge">ColorPickers</code> outside the <code class="language-plaintext highlighter-rouge">ListView</code> that display colours associated with the item selected in the <code class="language-plaintext highlighter-rouge">ListView</code>.  There is code that updates the value in these <code class="language-plaintext highlighter-rouge">ColorPickers</code> when an <code class="language-plaintext highlighter-rouge">ManifoldEvent</code> indicating that a new <code class="language-plaintext highlighter-rouge">Manifold</code> has been selected.  Also, when the value in the <code class="language-plaintext highlighter-rouge">Spinners</code> is changed, it fires a <code class="language-plaintext highlighter-rouge">ManifoldEvent</code> that presumably gets handled externally and updates the <code class="language-plaintext highlighter-rouge">Manifold</code>.</p>

<p>If all of this seems very confusing and roundabout, that’s because it is.  This is one of those cases where doing it wrong is so, so much more complicated than doing it right.  We’ll see how this works in the next section.</p>

<h2 id="the-new-design">The New Design</h2>

<p>The new version uses <a href="/javafx/mvci/">MVCI</a> as a framework to implement a Reactive design.  There is a Presentation Model composed of <code class="language-plaintext highlighter-rouge">Observable</code> data classes which are then bound (mostly bi-directionally) to the value <code class="language-plaintext highlighter-rouge">Properties</code> of the screen <code class="language-plaintext highlighter-rouge">Nodes</code>.  This means that the layout elements are instantiated, configured, bound to the Presentation Model and then added to the layout and forgotten.  There is no need to ever reference them again, as everything important about them has been bound to <code class="language-plaintext highlighter-rouge">Properties</code> in the Presentation Model.</p>

<p>There is no need to “scrape” data out of the GUI <code class="language-plaintext highlighter-rouge">Nodes</code>.</p>

<p>All of the application logic has been moved out of the layout code and into the Interactor.  The Interactor is suprisingly small, because much of complexity of communication with the external screen has now been eliminated through shared data.</p>

<p>There is a <code class="language-plaintext highlighter-rouge">SharedModel</code> class which contains JavaFX <code class="language-plaintext highlighter-rouge">Observable</code> objects that are supplied by whatever element of the overall application contains this screen.</p>

<p>The Controller, as usual, handles instantiation, threading and connectivity to the rest of the application’s GUI.</p>

<h3 id="the-listviews-1">The ListViews</h3>

<p>This was the biggest architectural change to the design.  The <code class="language-plaintext highlighter-rouge">Manifold</code> and <code class="language-plaintext highlighter-rouge">Distance</code> domain objects were transformed into Presentation Objects and all of the singleton related stuff was removed.  Now they are simple JavaFX Observable POJO`s.</p>

<p>The <code class="language-plaintext highlighter-rouge">ListViews</code> were changed to have custom <code class="language-plaintext highlighter-rouge">ListCells</code> that mirrored the structure in the <code class="language-plaintext highlighter-rouge">ManifoldListItem</code> and <code class="language-plaintext highlighter-rouge">DistanceListItem</code> classes.  These <code class="language-plaintext highlighter-rouge">ListCells</code> contain interactive <code class="language-plaintext highlighter-rouge">Nodes</code> like <code class="language-plaintext highlighter-rouge">TextField</code> and <code class="language-plaintext highlighter-rouge">CheckBox</code> who’s values are bi-directionally bound to the corresponding <code class="language-plaintext highlighter-rouge">Properties</code> in the item currently loaded into the <code class="language-plaintext highlighter-rouge">ListCell</code>.</p>

<p>To handle the values inside <code class="language-plaintext highlighter-rouge">Manifold</code> and <code class="language-plaintext highlighter-rouge">Distance</code> that are updated from outside the <code class="language-plaintext highlighter-rouge">ListView</code>, the Model has two <code class="language-plaintext highlighter-rouge">Properties</code>:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">selectedDistance</span><span class="p">:</span> <span class="nc">ObjectProperty</span><span class="p">&lt;</span><span class="nc">Distance</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">SimpleObjectProperty</span><span class="p">()</span>
<span class="kd">val</span> <span class="py">selectedManifold</span><span class="p">:</span> <span class="nc">ObjectProperty</span><span class="p">&lt;</span><span class="nc">Manifold</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">SimpleObjectProperty</span><span class="p">()</span>
</code></pre></div></div>
<p>These need to be synchronized with the current selection in the <code class="language-plaintext highlighter-rouge">ListViews</code>.  Ordinarily you could do this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">infix</span> <span class="k">fun</span> <span class="p">&lt;</span><span class="nc">T</span> <span class="p">:</span> <span class="nc">Any</span><span class="p">&gt;</span> <span class="nf">ListView</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;.</span><span class="nf">bindSelection</span><span class="p">(</span><span class="n">boundProperty</span><span class="p">:</span> <span class="nc">ObjectProperty</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;):</span> <span class="nc">ListView</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nf">apply</span> <span class="p">{</span>
    <span class="n">boundProperty</span><span class="p">.</span><span class="nf">bind</span><span class="p">(</span><span class="n">selectionModel</span><span class="p">.</span><span class="nf">selectedItemProperty</span><span class="p">())</span>
<span class="p">}</span>
</code></pre></div></div>
<p>But there’s a twist here…</p>

<p>The selected item needs to be synchronized with an external selection of those objects from some other part of the application.  This needs to be in both directions.  That external component needs to be able to select these objects, and react to changes in the selection from these <code class="language-plaintext highlighter-rouge">ListViews</code>.  Ordinarily, you’d just make the bind bi-directional, but there’s an issue there:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="nc">ReadOnlyObjectProperty</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">selectedItem</span>
</code></pre></div></div>
<p>Oh no!  The <code class="language-plaintext highlighter-rouge">selectedItem</code> property in <code class="language-plaintext highlighter-rouge">SelectionModel</code> is read-only!  The only way to change the value programmatically is to call <code class="language-plaintext highlighter-rouge">SelectionModel.selectItem()</code>.  This means we’ll have to use subscriptions to handle the changes:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">infix</span> <span class="k">fun</span> <span class="p">&lt;</span><span class="nc">T</span> <span class="p">:</span> <span class="nc">Any</span><span class="p">&gt;</span> <span class="nf">ListView</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;.</span><span class="nf">connectSelection</span><span class="p">(</span><span class="n">connectedProperty</span><span class="p">:</span> <span class="nc">ObjectProperty</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;):</span> <span class="nc">ListView</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nf">apply</span> <span class="p">{</span>
    <span class="n">selectionModel</span><span class="p">.</span><span class="nf">selectedItemProperty</span><span class="p">().</span><span class="nf">subscribe</span> <span class="p">{</span> <span class="n">newItem</span> <span class="p">-&gt;</span> <span class="n">connectedProperty</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="n">newItem</span> <span class="p">}</span>
    <span class="n">connectedProperty</span><span class="p">.</span><span class="nf">subscribe</span> <span class="p">{</span> <span class="n">newItem</span> <span class="p">-&gt;</span> <span class="n">selectionModel</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="n">newItem</span><span class="p">)</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>If you think about it, you’ll realize that triggering the subscription on one <code class="language-plaintext highlighter-rouge">Property</code> due to a change in the other won’t cause an infinite loop because the result of that subscription will bring the two values in sync and won’t trigger the second subscription.</p>

<p>You can now see that this screen doesn’t even need to know about the domain objects <code class="language-plaintext highlighter-rouge">Manifold</code> and <code class="language-plaintext highlighter-rouge">Distance</code> any more.  These elements - if they exist at all - are created somewhere else and then presumably used to create <code class="language-plaintext highlighter-rouge">Manifold</code> and <code class="language-plaintext highlighter-rouge">Distance</code> presentation data objects.</p>

<h3 id="drag-n-drop-file-import">“Drag ‘n Drop” File Import</h3>

<p>Right near the top of the FXML Controller, there is some code that initialized the “root” of the Scene as the destination for a drag ‘n drop operation to load a configuration file.  The idea being, I assume, that you can just drag a file from a file manager application into this layout and it will load it in if it is, in fact, a configuration file.</p>

<p>I wrestled with the idea of putting the <code class="language-plaintext highlighter-rouge">EventHandlers</code> for drag and drop into the layout itself, but then realized that I’d have to supply an action handler for this to the ViewBuilder from the Controller.  The end result would be that I would have a <code class="language-plaintext highlighter-rouge">File</code> object floating around in my layout code, even if indirectly.  This part bothered me.</p>

<p>I came to the conclusion that the drag and drop didn’t have anything to do with the layout as a layout.  This would be different if there was a box in the layout that said “Drop Files Here” and only that box would respond to the drag and drop operation.  In this situation, we don’t have anything like this, and the entire layout can just be considered as a <code class="language-plaintext highlighter-rouge">Node</code> (in this case a <code class="language-plaintext highlighter-rouge">Region</code>, but we only care about it as a subclass of <code class="language-plaintext highlighter-rouge">Node</code>) and dealt with from the outside.</p>

<p>This means that it makes sense to add the drag and drop handling as a decorator onto the layout directly from the Controller.  Now, we don’t have to pass any handlers over to the ViewBuilder, since it isn’t involved.  In fact, neither the ViewBuilder or the layout itself has any knowledge that it is a drag and drop destination.</p>

<p>In the Controller, the <code class="language-plaintext highlighter-rouge">getView()</code> method now looks like this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">getView</span><span class="p">():</span> <span class="nc">Region</span> <span class="p">=</span> <span class="n">viewBuilder</span><span class="p">.</span><span class="nf">build</span><span class="p">()</span> <span class="nf">asFileDrop</span> <span class="p">{</span> <span class="nf">loadUmap</span><span class="p">(</span><span class="n">it</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="p">}</span>
</code></pre></div></div>
<p>It’s worth looking at the WidgetsFX implementation of this, since it is a great example of how the boilerplate can be stripped out of your application code:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">infix</span> <span class="k">fun</span> <span class="p">&lt;</span><span class="nc">T</span> <span class="p">:</span> <span class="nc">Node</span><span class="p">&gt;</span> <span class="nc">T</span><span class="p">.</span><span class="nf">asFileDrop</span><span class="p">(</span><span class="n">handler</span><span class="p">:</span> <span class="p">(</span><span class="nc">List</span><span class="p">&lt;</span><span class="nc">File</span><span class="p">&gt;)</span> <span class="p">-&gt;</span> <span class="nc">Unit</span><span class="p">):</span> <span class="nc">T</span> <span class="p">=</span> <span class="nf">apply</span> <span class="p">{</span>
    <span class="nf">addEventHandler</span><span class="p">(</span><span class="nc">DragEvent</span><span class="p">.</span><span class="nc">DRAG_OVER</span><span class="p">)</span> <span class="p">{</span> <span class="n">event</span> <span class="p">-&gt;</span>
        <span class="n">event</span><span class="p">.</span><span class="nf">acceptTransferModes</span><span class="p">(</span><span class="nc">TransferMode</span><span class="p">.</span><span class="nc">COPY</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="nf">addEventHandler</span><span class="p">(</span><span class="nc">DragEvent</span><span class="p">.</span><span class="nc">DRAG_DROPPED</span><span class="p">)</span> <span class="p">{</span> <span class="n">event</span> <span class="p">-&gt;</span>
        <span class="nf">with</span> <span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="n">dragboard</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="nf">hasFiles</span><span class="p">())</span> <span class="p">{</span>
                <span class="n">handler</span><span class="p">.</span><span class="nf">invoke</span><span class="p">(</span><span class="n">files</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Everything in here is boilerplate and every time you implement this you would need those 11 lines of code.  But now it just becomes <code class="language-plaintext highlighter-rouge">asFileDrop {do something}</code>.</p>

<h3 id="the-large-number-of-action-handlers">The Large Number of Action Handlers</h3>

<p>There’s a lot of <code class="language-plaintext highlighter-rouge">Buttons</code> in this screen, and all of them trigger some kind of action within the application logic.</p>

<div class="notice_question--primary">
 <img src="/assets/logos/BRAIN_Question.png" alt="primary" style="float:left;margin-right: 10px" />
 <p style="overflow:auto; float:none">
   How do we provide all these action handlers to the View?<br /><br />
 </p>
</div>

<p>The first thing to understand is that the Reactive nature of the new design means that all of the data that might be relevant to any action is already represented in the Model and is always fully up to date.</p>

<p class="notice--primary">This means that no action handler ever has to supply data from the GUI.  The only information that is required is which <code class="language-plaintext highlighter-rouge">Button</code> was clicked.</p>

<p>The few actions that need to open <code class="language-plaintext highlighter-rouge">FileChoosers</code> need to pass the <code class="language-plaintext highlighter-rouge">Window (Stage)</code> that triggered it as <code class="language-plaintext highlighter-rouge">FileChooser</code> needs this to work.  I chose to implement these such that the <code class="language-plaintext highlighter-rouge">Button</code> would grab this element as part of its <code class="language-plaintext highlighter-rouge">EventHandler</code> that runs at click-time.  These actions do need to pass that element back to the action handler.</p>

<p>Two <code class="language-plaintext highlighter-rouge">Enums</code> were created, one for the <code class="language-plaintext highlighter-rouge">FileChooser</code> operations, called <code class="language-plaintext highlighter-rouge">FileOperation</code> and the other for all the other operations, called <code class="language-plaintext highlighter-rouge">GeneralOperation</code>.  “Operation” seems less likely to get confused with “Action”.  Naming things is hard!  The action handler is a <code class="language-plaintext highlighter-rouge">Consumer&lt;GeneralOperation&gt;</code>, or in Kotlin notation <code class="language-plaintext highlighter-rouge">(GeneralOperation) -&gt; Unit</code>, and each <code class="language-plaintext highlighter-rouge">Button</code> will call the handler’s <code class="language-plaintext highlighter-rouge">invoke(GeneralOperation)</code> method, passing the appropriate <code class="language-plaintext highlighter-rouge">GeneralOperation</code> value to it.</p>

<p>It is the Controller’s job to define this action handler and provide it to the ViewBuilder.  Unless there is some thread handling required in the nature of any of these actions, the Controller has no business getting involved in the execution of these actions, which is the domain of the Interactor.  This action handler is essential a “dispatch routine”, which invokes an appropriate Interactor method for each operation type via the Kotlin version of <code class="language-plaintext highlighter-rouge">switch</code>:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">fun</span> <span class="nf">generalOperation</span><span class="p">(</span><span class="n">operation</span><span class="p">:</span> <span class="nc">GeneralOperation</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">when</span> <span class="p">(</span><span class="n">operation</span><span class="p">)</span> <span class="p">{</span>
        <span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">BUILD_CLUSTER</span> <span class="p">-&gt;</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">buildCluster</span><span class="p">()</span>
        <span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">GENERATE</span> <span class="p">-&gt;</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">generate</span><span class="p">()</span>
        <span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">CLEAR_ALL</span> <span class="p">-&gt;</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">clearAll</span><span class="p">()</span>
        <span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">EXPORT_ALL</span> <span class="p">-&gt;</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">exportAll</span><span class="p">()</span>
        <span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">CLEAR_DISTANCES</span> <span class="p">-&gt;</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">clearDistances</span><span class="p">()</span>
        <span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">PROJECT</span> <span class="p">-&gt;</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">project</span><span class="p">()</span>
        <span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">EXPORT_MATRIX</span> <span class="p">-&gt;</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">exportMatrix</span><span class="p">()</span>
        <span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">SAVE_PROJECTIONS</span> <span class="p">-&gt;</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">saveProjections</span><span class="p">()</span>
        <span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">RUN_PCA</span> <span class="p">-&gt;</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">runPca</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>For all practical purposes, most of these operations do not have any real application logic associated with them that resides inside of this MVCI framework.  They instead are triggering actions elsewhere in the application.  We’ll look at that next…</p>

<h3 id="connecting-to-the-rest-of-the-application-1">Connecting to the Rest of the Application</h3>

<p>There is a <code class="language-plaintext highlighter-rouge">SharedElements</code> class which contains JavaFX <code class="language-plaintext highlighter-rouge">Observable</code> objects that are supplied by whatever element of the overall application contains this screen.  This is the main source of coupling to the rest of the application’s GUI.  Sharing these data elements has the magic effect of making all the <code class="language-plaintext highlighter-rouge">Event</code> firing and <code class="language-plaintext highlighter-rouge">EventHandlers</code> in the original version totally redundant.</p>

<p>Let’s look at how this works…</p>

<p>In a situation where this screen is part of a larger application, whatever GUI element of that application that “owns” this screen would pass a <code class="language-plaintext highlighter-rouge">SharedElements</code> object to the Controller via its constructor.  This <code class="language-plaintext highlighter-rouge">SharedElements</code> object is then passed to the Model via its constructor.  At this point, the Controller’s involvement with the SharedElements is done.</p>

<p><code class="language-plaintext highlighter-rouge">SharedElements</code> is a set of <code class="language-plaintext highlighter-rouge">Properties</code> which are defined externally.  These are used to instantiate the <code class="language-plaintext highlighter-rouge">Property</code> fields in the Model.  Like this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">showWireFrame</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="n">sharedElements</span><span class="p">.</span><span class="n">showWireFrame</span>
<span class="kd">val</span> <span class="py">showControlPoints</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="n">sharedElements</span><span class="p">.</span><span class="n">showControlPoints</span>
</code></pre></div></div>
<p>It’s important to note that since these elements are incorporated into the Model, just like any other elements, the fact that these are <code class="language-plaintext highlighter-rouge">Properties</code> defined externally is never exposed to the View or the Interactor.  Any changes to the manner in which these are implemented, connected or related to the <code class="language-plaintext highlighter-rouge">Properties</code> in the Model are never going to ripple through to be changes in the View or the Interactor.</p>

<p>In a similar manner an object of the class <code class="language-plaintext highlighter-rouge">SharedFunctions</code> is passed to the constructor of the Controller, which calls it <code class="language-plaintext highlighter-rouge">externalFunctions</code>.  This is, in turn, passed to the Interactor via its constructor.  At this point, the Controller’s involvement with <code class="language-plaintext highlighter-rouge">SharedFunctions</code> is complete.</p>

<p>The Interactor then invokes these external functions as part of the application logic related to performing various actions related to <code class="language-plaintext highlighter-rouge">Button</code> clicks in the View.</p>

<p>On the surface this seem very round-about but it actually isolates the coupling nicely.  The View has a <code class="language-plaintext highlighter-rouge">Button</code> that triggers an <code class="language-plaintext highlighter-rouge">EventHandler</code>.  That <code class="language-plaintext highlighter-rouge">EventHandler</code> invokes an action <code class="language-plaintext highlighter-rouge">Consumer</code> that is defined by the Controller.  That <code class="language-plaintext highlighter-rouge">Consumer</code> invokes an corresponding method in the Interactor which, in turn, invokes a <code class="language-plaintext highlighter-rouge">Runnable</code> provided to the Interactor from outside the MVCI construct through the Controller.</p>

<p>Information about what these pieces do is available on a strict “need to know” basis throughout the framework.  There is an <code class="language-plaintext highlighter-rouge">Enum</code> which is shared between the Controller and the ViewBuilder that defines what operations can be invoked from the View, but the View has no idea what those operaitons do.  The Controller knows how to invoke corresponding methods in the Interactor, but has no idea what those methods do.  The Interactor knows which external operations to call, but it has no idea what they do.  Finally, the Controller passes the list external operation handlers to the Interactor, but doesn’t know what they are.</p>

<h1 id="counting-lines-of-codefxml">Counting Lines of Code/FXML</h1>

<p>I’m not generally a fan of counting code, but it can be an indicator of the complexity, readability and maintainability of a system.</p>

<p>Let’s look at the original:</p>

<table>
  <thead>
    <tr>
      <th>Element</th>
      <th>Lines</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>FXML</td>
      <td>462</td>
    </tr>
    <tr>
      <td>FXML Controller</td>
      <td>752</td>
    </tr>
    <tr>
      <td>ManifoldListItem</td>
      <td>76</td>
    </tr>
    <tr>
      <td>DistanceListItem</td>
      <td>55</td>
    </tr>
    <tr>
      <td>Manifold</td>
      <td>130</td>
    </tr>
    <tr>
      <td>Distance</td>
      <td>190</td>
    </tr>
    <tr>
      <td>Total</td>
      <td>1665</td>
    </tr>
  </tbody>
</table>

<p>Now, let’s look at the hand-coded version:</p>

<table>
  <thead>
    <tr>
      <th>Element</th>
      <th>Lines</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Model</td>
      <td>60</td>
    </tr>
    <tr>
      <td>Controller</td>
      <td>48</td>
    </tr>
    <tr>
      <td>Interactor</td>
      <td>80</td>
    </tr>
    <tr>
      <td>ViewBuilder</td>
      <td>230</td>
    </tr>
    <tr>
      <td>ManifoldListCell</td>
      <td>21</td>
    </tr>
    <tr>
      <td>DistanceListCell</td>
      <td>24</td>
    </tr>
    <tr>
      <td>SharedElements</td>
      <td>12</td>
    </tr>
    <tr>
      <td>SharedFunctions</td>
      <td>12</td>
    </tr>
    <tr>
      <td>Total</td>
      <td>487</td>
    </tr>
  </tbody>
</table>

<p>The hand-coded version is less than 1/3 the amount of code in the original version - although I’m not sure how compare FXML to lines of code.  In any event, the entire hand-coded version is only slightly larger than the FXML file itself.</p>

<h1 id="is-it-easier-to-understand">Is it Easier to Understand?</h1>

<p>I always feel that the most common use case for someone performing maintenance or enhancement to a layout is going to start out by looking at the actual running screen.  Then they are going to want to get a feel for how some specific section of the layout is designed.</p>

<p>In this case, we have a bunch of <code class="language-plaintext highlighter-rouge">Tabs</code> and the programmer is probably going to want to drill down into the code for a specific <code class="language-plaintext highlighter-rouge">Tab</code>.</p>

<p>Let’s take a look at the top of the layout code, were the root elements are defined:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">override</span> <span class="k">fun</span> <span class="nf">build</span><span class="p">():</span> <span class="nc">Region</span> <span class="p">=</span> <span class="nc">TabPane</span><span class="p">(</span>
    <span class="nf">createUmapTab</span><span class="p">(),</span>
    <span class="nf">createPcaTab</span><span class="p">(),</span>
    <span class="nf">createDistancesTab</span><span class="p">(),</span>
    <span class="nf">createHullTab</span><span class="p">()</span>
<span class="p">)</span> <span class="n">withClosingPolicy</span> <span class="nc">TabPane</span><span class="p">.</span><span class="nc">TabClosingPolicy</span><span class="p">.</span><span class="nc">UNAVAILABLE</span>

<span class="k">private</span> <span class="k">fun</span> <span class="nf">createPcaTab</span><span class="p">()</span> <span class="p">=</span> <span class="nc">Tab</span><span class="p">(</span><span class="s">"PCA"</span><span class="p">)</span> <span class="nf">withContents</span> <span class="p">(</span><span class="nc">VBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">,</span> <span class="nf">pcaGridPane</span><span class="p">(),</span> <span class="nf">pcaButtonBox</span><span class="p">())</span> <span class="n">padWith</span> <span class="mf">25.0</span><span class="p">)</span>

<span class="k">private</span> <span class="k">fun</span> <span class="nf">createUmapTab</span><span class="p">()</span> <span class="p">=</span> <span class="nc">Tab</span><span class="p">(</span><span class="s">"UMAP"</span><span class="p">)</span> <span class="n">withContents</span> <span class="nc">VBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">padWith</span><span class="p">(</span><span class="mf">25.0</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">addChild</span><span class="p">(</span><span class="nf">umapGridPane</span><span class="p">())</span>
    <span class="p">.</span><span class="nf">addChild</span><span class="p">(</span><span class="nf">umapDistanceThresholdBox</span><span class="p">())</span>
    <span class="p">.</span><span class="nf">addChild</span><span class="p">(</span><span class="nf">umapControlBox</span><span class="p">())</span>

<span class="k">private</span> <span class="k">fun</span> <span class="nf">createDistancesTab</span><span class="p">()</span> <span class="p">=</span> <span class="nc">Tab</span><span class="p">(</span><span class="s">"Distances"</span><span class="p">)</span> <span class="n">withContents</span> <span class="nc">HBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">,</span> <span class="nf">distanceLeft</span><span class="p">(),</span> <span class="nf">distanceRight</span><span class="p">())</span>

<span class="k">private</span> <span class="k">fun</span> <span class="nf">createHullTab</span><span class="p">()</span> <span class="p">=</span> <span class="nc">Tab</span><span class="p">(</span><span class="s">"Hull Geometry"</span><span class="p">)</span> <span class="n">withContents</span> <span class="nc">BorderPane</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">withTop</span><span class="p">(</span><span class="nf">hullTop</span><span class="p">())</span>
    <span class="p">.</span><span class="nf">withLeft</span><span class="p">(</span><span class="nf">manifoldPropertyBox</span><span class="p">())</span>
    <span class="p">.</span><span class="nf">withCenter</span><span class="p">(</span><span class="nf">manifoldBox</span><span class="p">())</span>
</code></pre></div></div>
<p>We can see right away that the entirety of the layout is a single <code class="language-plaintext highlighter-rouge">TabPane</code> with 4 <code class="language-plaintext highlighter-rouge">Tabs</code> that cannot be closed.  We have a short builder method for each <code class="language-plaintext highlighter-rouge">Tab</code> with an name that mirrors the title of each <code class="language-plaintext highlighter-rouge">Tab</code>.  No matter which <code class="language-plaintext highlighter-rouge">Tab</code> you are interested in you can quickly find it, without even having to scroll down through the code.</p>

<p>We can quickly get an idea about the structure of the contents of these <code class="language-plaintext highlighter-rouge">Tabs</code>.  Two are <code class="language-plaintext highlighter-rouge">VBoxes</code>, one is an <code class="language-plaintext highlighter-rouge">HBox</code> and the other is a <code class="language-plaintext highlighter-rouge">BorderPane</code>.  There are builders for every element contained in these <code class="language-plaintext highlighter-rouge">Regions</code> in the <code class="language-plaintext highlighter-rouge">Tabs</code>, and we can click-through on them to get to them.</p>

<p>You can also see here that all of the configuration elements such <code class="language-plaintext highlighter-rouge">setPadding()</code> have been implemented as extension decorator functions that have also been implemented as “infix” functions.  This means that they can be used without the <code class="language-plaintext highlighter-rouge">.</code> and <code class="language-plaintext highlighter-rouge">()</code> and can, in some cases, increase readability.</p>

<p>In some cases, it looks cleaner if the dot notation is used instead.  This allows the decorators to be stacked vertically when they start to add up.  However, when the composition is trivial, then the infix notation keeps the coding trivial.  Compare the “PCA” <code class="language-plaintext highlighter-rouge">Tab</code> to the “UMAP” <code class="language-plaintext highlighter-rouge">Tab</code>.</p>

<p>I’ve tried to avoid naming the builders with positional names whenever possible.  However, I really don’t know what this application does, so it was hard to guess at good names for some builders.  I gave up with the builder for the layout in <code class="language-plaintext highlighter-rouge">BorderPane.top</code> in the “Hull” <code class="language-plaintext highlighter-rouge">Tab</code>, and I just called it <code class="language-plaintext highlighter-rouge">hullTop()</code>.</p>

<p>I find that <code class="language-plaintext highlighter-rouge">GridPanes</code> are always clumsy to deal with, no matter what, and the row/column locations never jump out at you when scanning the code.  However, you can organize the code to make it easier to find things.  I tried this with the “UMAP” <code class="language-plaintext highlighter-rouge">GridPane</code>:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">fun</span> <span class="nf">umapGridPane</span><span class="p">()</span> <span class="p">=</span> <span class="nc">GridPane</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
    <span class="n">columnConstraints</span> <span class="p">+=</span> <span class="nc">ColumnConstraints</span><span class="p">(</span><span class="mf">10.0</span><span class="p">,</span> <span class="mf">200.0</span><span class="p">,</span> <span class="mf">288.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span> <span class="n">hgrow</span> <span class="p">=</span> <span class="nc">Priority</span><span class="p">.</span><span class="nc">SOMETIMES</span> <span class="p">}</span>
    <span class="n">columnConstraints</span> <span class="p">+=</span> <span class="nc">ColumnConstraints</span><span class="p">(</span><span class="mf">10.0</span><span class="p">,</span> <span class="mf">200.0</span><span class="p">,</span> <span class="mf">380.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span> <span class="n">hgrow</span> <span class="p">=</span> <span class="nc">Priority</span><span class="p">.</span><span class="nc">SOMETIMES</span> <span class="p">}</span>
    <span class="nf">addRowConstraints</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="n">stdRowConst</span><span class="p">)</span>
    <span class="nf">umapSpinnerColumn</span><span class="p">()</span>
    <span class="nf">umapSliderColumn</span><span class="p">()</span>
<span class="p">}</span>

<span class="k">private</span> <span class="k">fun</span> <span class="nc">GridPane</span><span class="p">.</span><span class="nf">umapSliderColumn</span><span class="p">()</span> <span class="p">{</span>
    <span class="nf">stackedSlider</span><span class="p">(</span><span class="s">"Repulsion Strength"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">repulsionStr</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="mf">2.0</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="n">ticksOn</span> <span class="k">true</span> <span class="n">addStyle</span> <span class="s">"std-slider"</span>
    <span class="nf">stackedSlider</span><span class="p">(</span><span class="s">"Minimum Distance"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">minimumDistance</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="mf">0.6</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="n">ticksOn</span> <span class="k">true</span> <span class="n">addStyle</span> <span class="s">"std-slider"</span>
    <span class="nf">stackedSlider</span><span class="p">(</span><span class="s">"Spread"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">spread</span><span class="p">,</span> <span class="mf">0.5</span><span class="p">,</span> <span class="mf">1.5</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span> <span class="n">ticksOn</span> <span class="k">true</span> <span class="n">addStyle</span> <span class="s">"std-slider"</span>
    <span class="nf">stackedSlider</span><span class="p">(</span><span class="s">"Op Mix Ration"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">opMixRatio</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">6</span><span class="p">)</span> <span class="n">ticksOn</span> <span class="k">true</span> <span class="n">addStyle</span> <span class="s">"std-slider"</span>
    <span class="nf">stackedSlider</span><span class="p">(</span><span class="s">"Target Weight"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">targetWeight</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">8</span><span class="p">)</span> <span class="n">ticksOn</span> <span class="k">true</span> <span class="n">addStyle</span> <span class="s">"std-slider"</span>
<span class="p">}</span>

<span class="k">private</span> <span class="k">fun</span> <span class="nc">GridPane</span><span class="p">.</span><span class="nf">umapSpinnerColumn</span><span class="p">()</span> <span class="p">{</span>
    <span class="nf">stackedIntSpinner</span><span class="p">(</span><span class="s">"Number of Components"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">numberOfComponents</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="n">addStyle</span> <span class="s">"std-spinner"</span>
    <span class="nf">stackedIntSpinner</span><span class="p">(</span><span class="s">"Number of Epochs"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">numberOfEpochs</span><span class="p">,</span> <span class="mi">25</span><span class="p">,</span> <span class="mi">500</span><span class="p">,</span> <span class="mi">25</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="n">addStyle</span> <span class="s">"std-spinner"</span>
    <span class="nf">stackedIntSpinner</span><span class="p">(</span><span class="s">"Nearest Neighbours"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">nearestNeighbour</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">500</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span> <span class="n">addStyle</span> <span class="s">"std-spinner"</span>
    <span class="nf">stackedIntSpinner</span><span class="p">(</span><span class="s">"Negative Sample Rate"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">negativeSampleRate</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">250</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">6</span><span class="p">)</span> <span class="n">addStyle</span> <span class="s">"std-spinner"</span>
    <span class="nf">stackedIntSpinner</span><span class="p">(</span><span class="s">"Local Connectivity"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">localConnectivity</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">250</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">8</span><span class="p">)</span> <span class="n">addStyle</span> <span class="s">"std-spinner"</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This <code class="language-plaintext highlighter-rouge">GridPane</code> has two columns, the one on the right has a bunch of <code class="language-plaintext highlighter-rouge">Sliders</code> with their <code class="language-plaintext highlighter-rouge">Labels</code>, and the one on the left has <code class="language-plaintext highlighter-rouge">Spinners</code> and their <code class="language-plaintext highlighter-rouge">Labels</code>.  In both columns, the <code class="language-plaintext highlighter-rouge">Label</code> sits on the row above its corresponding input <code class="language-plaintext highlighter-rouge">Control</code>.  It’s now easy to see the structure of the <code class="language-plaintext highlighter-rouge">GridPane</code> at a glance.  In contrast, translating these <code class="language-plaintext highlighter-rouge">GridPanes</code> from the FXML took more time than any other part because you couldn’t just look at it quickly and understand the structure.</p>

<p>It’s not clear when you see the code in these articles that the IDE that I use (Intellij IDEA) provides a lot of on-screen information that’s not seen here.  For instance, this is what I see with <code class="language-plaintext highlighter-rouge">umapSliderColumn()</code>:</p>

<p><img src="/assets/images/TrinityScreenShot2.png" alt="Intellij Snapshot" /></p>

<p>From here it is clear what all of those parameters do.</p>

<p>One further thing, which I think contributes greatly to the “easier to understand” aspect of this discussion.  You can see that <strong>ALL</strong> of the parameters related to these input <code class="language-plaintext highlighter-rouge">Controls</code> are included here in these 10 method calls.  There’s no need to go running off somewhere else see how one of the <code class="language-plaintext highlighter-rouge">Spinners</code> is configured.  Each one is also bi-directionally bound to a <code class="language-plaintext highlighter-rouge">Property</code> field in the Model, so there’s no need to mess about with initial values either - as that is handled in the Model or the Interactor.</p>

<h1 id="the-new-code">The New Code</h1>

<p>Oh, wow!  That’s a lot of discussion and preamble, and not a lot of coding.  Let’s take a look at the completed redesign.</p>

<h2 id="the-viewbuilder">The ViewBuilder</h2>

<p>This is the bulk of the code…</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">FromFxmlViewBuilder</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">model</span><span class="p">:</span> <span class="nc">FromFxmlModel</span><span class="p">,</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">fileOp</span><span class="p">:</span> <span class="p">(</span><span class="nc">Window</span><span class="p">,</span> <span class="nc">FileOperation</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Unit</span><span class="p">,</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">genOp</span><span class="p">:</span> <span class="p">(</span><span class="nc">GeneralOperation</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Unit</span>
<span class="p">)</span> <span class="p">:</span>
    <span class="nc">Builder</span><span class="p">&lt;</span><span class="nc">Region</span><span class="p">&gt;</span> <span class="p">{</span>

    <span class="k">private</span> <span class="kd">val</span> <span class="py">stdRowConst</span> <span class="p">=</span> <span class="nc">RowConstraints</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="n">minHeight</span> <span class="p">=</span> <span class="mf">10.0</span>
        <span class="n">prefHeight</span> <span class="p">=</span> <span class="mf">30.0</span>
        <span class="n">vgrow</span> <span class="p">=</span> <span class="nc">Priority</span><span class="p">.</span><span class="nc">NEVER</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">build</span><span class="p">():</span> <span class="nc">Region</span> <span class="p">=</span> <span class="nc">TabPane</span><span class="p">(</span><span class="nf">createUmapTab</span><span class="p">(),</span> <span class="nf">createPcaTab</span><span class="p">(),</span> <span class="nf">createDistancesTab</span><span class="p">(),</span> <span class="nf">createHullTab</span><span class="p">())</span>
        <span class="p">.</span><span class="nf">withClosingPolicy</span><span class="p">(</span><span class="nc">TabPane</span><span class="p">.</span><span class="nc">TabClosingPolicy</span><span class="p">.</span><span class="nc">UNAVAILABLE</span><span class="p">)</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">createPcaTab</span><span class="p">()</span> <span class="p">=</span> <span class="nc">Tab</span><span class="p">(</span><span class="s">"PCA"</span><span class="p">)</span> <span class="nf">withContents</span> <span class="p">(</span><span class="nc">VBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">,</span> <span class="nf">pcaGridPane</span><span class="p">(),</span> <span class="nf">pcaButtonBox</span><span class="p">())</span> <span class="n">padWith</span> <span class="mf">25.0</span><span class="p">)</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">createUmapTab</span><span class="p">()</span> <span class="p">=</span> <span class="nc">Tab</span><span class="p">(</span><span class="s">"UMAP"</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">withContents</span><span class="p">(</span><span class="nc">VBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">,</span> <span class="nf">umapGridPane</span><span class="p">(),</span> <span class="nf">umapDistanceThresholdBox</span><span class="p">(),</span> <span class="nf">umapControlBox</span><span class="p">()).</span><span class="nf">padWith</span><span class="p">(</span><span class="mf">25.0</span><span class="p">))</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">createDistancesTab</span><span class="p">()</span> <span class="p">=</span> <span class="nc">Tab</span><span class="p">(</span><span class="s">"Distances"</span><span class="p">)</span> <span class="n">withContents</span> <span class="nc">HBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">,</span> <span class="nf">distanceLeft</span><span class="p">(),</span> <span class="nf">distanceRight</span><span class="p">())</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">createHullTab</span><span class="p">()</span> <span class="p">=</span> <span class="nc">Tab</span><span class="p">(</span><span class="s">"Hull Geometry"</span><span class="p">)</span> <span class="n">withContents</span> <span class="nc">BorderPane</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">withTop</span><span class="p">(</span><span class="nf">hullTop</span><span class="p">())</span>
        <span class="p">.</span><span class="nf">withLeft</span><span class="p">(</span><span class="nf">manifoldPropertyBox</span><span class="p">())</span>
        <span class="p">.</span><span class="nf">withCenter</span><span class="p">(</span><span class="nf">manifoldBox</span><span class="p">())</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">umapGridPane</span><span class="p">()</span> <span class="p">=</span> <span class="nc">GridPane</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="n">columnConstraints</span> <span class="p">+=</span> <span class="nc">ColumnConstraints</span><span class="p">(</span><span class="mf">10.0</span><span class="p">,</span> <span class="mf">200.0</span><span class="p">,</span> <span class="mf">288.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span> <span class="n">hgrow</span> <span class="p">=</span> <span class="nc">Priority</span><span class="p">.</span><span class="nc">SOMETIMES</span> <span class="p">}</span>
        <span class="n">columnConstraints</span> <span class="p">+=</span> <span class="nc">ColumnConstraints</span><span class="p">(</span><span class="mf">10.0</span><span class="p">,</span> <span class="mf">200.0</span><span class="p">,</span> <span class="mf">380.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span> <span class="n">hgrow</span> <span class="p">=</span> <span class="nc">Priority</span><span class="p">.</span><span class="nc">SOMETIMES</span> <span class="p">}</span>
        <span class="nf">addRowConstraints</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="n">stdRowConst</span><span class="p">)</span>
        <span class="nf">umapSpinnerColumn</span><span class="p">()</span>
        <span class="nf">umapSliderColumn</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nc">GridPane</span><span class="p">.</span><span class="nf">umapSliderColumn</span><span class="p">()</span> <span class="p">{</span>
        <span class="nf">stackedSlider</span><span class="p">(</span><span class="s">"Repulsion Strength"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">repulsionStr</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="mf">2.0</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="n">ticksOn</span> <span class="k">true</span> <span class="n">addStyle</span> <span class="s">"std-slider"</span>
        <span class="nf">stackedSlider</span><span class="p">(</span><span class="s">"Minimum Distance"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">minimumDistance</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="mf">0.6</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="n">ticksOn</span> <span class="k">true</span> <span class="n">addStyle</span> <span class="s">"std-slider"</span>
        <span class="nf">stackedSlider</span><span class="p">(</span><span class="s">"Spread"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">spread</span><span class="p">,</span> <span class="mf">0.5</span><span class="p">,</span> <span class="mf">1.5</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span> <span class="n">ticksOn</span> <span class="k">true</span> <span class="n">addStyle</span> <span class="s">"std-slider"</span>
        <span class="nf">stackedSlider</span><span class="p">(</span><span class="s">"Op Mix Ration"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">opMixRatio</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">6</span><span class="p">)</span> <span class="n">ticksOn</span> <span class="k">true</span> <span class="n">addStyle</span> <span class="s">"std-slider"</span>
        <span class="nf">stackedSlider</span><span class="p">(</span><span class="s">"Target Weight"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">targetWeight</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">8</span><span class="p">)</span> <span class="n">ticksOn</span> <span class="k">true</span> <span class="n">addStyle</span> <span class="s">"std-slider"</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nc">GridPane</span><span class="p">.</span><span class="nf">umapSpinnerColumn</span><span class="p">()</span> <span class="p">{</span>
        <span class="nf">stackedIntSpinner</span><span class="p">(</span><span class="s">"Number of Components"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">numberOfComponents</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="n">addStyle</span> <span class="s">"std-spinner"</span>
        <span class="nf">stackedIntSpinner</span><span class="p">(</span><span class="s">"Number of Epochs"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">numberOfEpochs</span><span class="p">,</span> <span class="mi">25</span><span class="p">,</span> <span class="mi">500</span><span class="p">,</span> <span class="mi">25</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="n">addStyle</span> <span class="s">"std-spinner"</span>
        <span class="nf">stackedIntSpinner</span><span class="p">(</span><span class="s">"Nearest Neighbours"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">nearestNeighbour</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">500</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span> <span class="n">addStyle</span> <span class="s">"std-spinner"</span>
        <span class="nf">stackedIntSpinner</span><span class="p">(</span><span class="s">"Negative Sample Rate"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">negativeSampleRate</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">250</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">6</span><span class="p">)</span> <span class="n">addStyle</span> <span class="s">"std-spinner"</span>
        <span class="nf">stackedIntSpinner</span><span class="p">(</span><span class="s">"Local Connectivity"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">localConnectivity</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">250</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">8</span><span class="p">)</span> <span class="n">addStyle</span> <span class="s">"std-spinner"</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">umapDistanceThresholdBox</span><span class="p">()</span> <span class="p">=</span> <span class="nc">HBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="n">alignment</span> <span class="p">=</span> <span class="nc">Pos</span><span class="p">.</span><span class="nc">TOP_CENTER</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nc">VBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">,</span> <span class="nf">promptOf</span><span class="p">(</span><span class="s">"Distance Metric"</span><span class="p">),</span> <span class="nc">ChoiceBox</span><span class="p">(</span><span class="nf">generateDefaultMetrics</span><span class="p">()).</span><span class="nf">firstSelected</span><span class="p">())</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nc">VBox</span><span class="p">(</span>
            <span class="mf">10.0</span><span class="p">,</span>
            <span class="nf">promptOf</span><span class="p">(</span><span class="s">"Threshold (if applicable)"</span><span class="p">),</span>
            <span class="nf">doubleSpinnerOf</span><span class="p">(</span><span class="mf">0.01</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="s">"standard-spinner"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">threshold</span><span class="p">.</span><span class="nf">asObject</span><span class="p">()).</span><span class="nf">setStep</span><span class="p">(</span><span class="mf">0.01</span><span class="p">)</span>
        <span class="p">)</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">umapControlBox</span><span class="p">()</span> <span class="p">=</span> <span class="nc">HBox</span><span class="p">(</span><span class="mf">15.0</span><span class="p">,</span> <span class="nf">umapConfigButtonBox</span><span class="p">(),</span> <span class="nf">umapHyperBox</span><span class="p">(),</span> <span class="nf">umapRunExportButtonBox</span><span class="p">())</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">umapConfigButtonBox</span><span class="p">()</span> <span class="p">=</span> <span class="nc">VBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nf">buttonOf</span><span class="p">(</span><span class="s">"Load New Config"</span><span class="p">)</span> <span class="p">{</span> <span class="nf">println</span><span class="p">(</span><span class="s">"Hello"</span><span class="p">)</span> <span class="p">}</span> <span class="n">addStyle</span> <span class="s">"standard-button"</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nf">buttonOf</span><span class="p">(</span><span class="s">"Save Current Config"</span><span class="p">)</span> <span class="p">{</span> <span class="n">fileOp</span><span class="p">.</span><span class="nf">invoke</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">scene</span><span class="p">.</span><span class="n">window</span><span class="p">,</span> <span class="nc">FileOperation</span><span class="p">.</span><span class="nc">UMAP_SAVE</span><span class="p">)</span> <span class="p">}</span>
            <span class="p">.</span><span class="nf">addStyle</span><span class="p">(</span><span class="s">"standard-button"</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">umapHyperBox</span><span class="p">()</span> <span class="p">=</span> <span class="nc">VBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="nf">with</span><span class="p">(</span><span class="nc">ToggleGroup</span><span class="p">())</span> <span class="p">{</span>
            <span class="nf">addChild</span><span class="p">(</span><span class="nc">RadioButton</span><span class="p">(</span><span class="s">"Use Hypersurface"</span><span class="p">)</span> <span class="n">inToggleGroup</span> <span class="k">this</span><span class="p">)</span>
            <span class="nf">addChild</span><span class="p">(</span><span class="nc">RadioButton</span><span class="p">(</span><span class="s">"Use Hyperspace"</span><span class="p">)</span> <span class="n">inToggleGroup</span> <span class="k">this</span> <span class="n">setSelected</span> <span class="k">true</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="nf">addChild</span><span class="p">(</span><span class="nc">CheckBox</span><span class="p">(</span><span class="s">"Progress Output"</span><span class="p">))</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">umapRunExportButtonBox</span><span class="p">()</span> <span class="p">=</span> <span class="nc">VBox</span><span class="p">(</span>
        <span class="mf">10.0</span><span class="p">,</span>
        <span class="nf">buttonOf</span><span class="p">(</span><span class="s">"Run UMAP"</span><span class="p">)</span> <span class="p">{</span> <span class="n">genOp</span><span class="p">.</span><span class="nf">invoke</span><span class="p">(</span><span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">PROJECT</span><span class="p">)</span> <span class="p">}</span> <span class="n">addStyle</span> <span class="s">"standard-button"</span><span class="p">,</span>
        <span class="nf">buttonOf</span><span class="p">(</span><span class="s">"Export TMatrix"</span><span class="p">)</span> <span class="p">{</span> <span class="n">genOp</span><span class="p">.</span><span class="nf">invoke</span><span class="p">(</span><span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">EXPORT_MATRIX</span><span class="p">)</span> <span class="p">}</span> <span class="n">addStyle</span> <span class="s">"standard-button"</span><span class="p">,</span>
        <span class="nf">buttonOf</span><span class="p">(</span><span class="s">"Export Projections"</span><span class="p">)</span> <span class="p">{</span> <span class="n">genOp</span><span class="p">.</span><span class="nf">invoke</span><span class="p">(</span><span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">SAVE_PROJECTIONS</span><span class="p">)</span> <span class="p">}</span> <span class="n">addStyle</span> <span class="s">"standard-button"</span>
    <span class="p">)</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">pcaButtonBox</span><span class="p">()</span> <span class="p">=</span> <span class="nc">HBox</span><span class="p">(</span>
        <span class="mf">15.0</span><span class="p">,</span>
        <span class="nf">buttonOf</span><span class="p">(</span><span class="s">"Project Data"</span><span class="p">)</span> <span class="p">{</span> <span class="n">genOp</span><span class="p">.</span><span class="nf">invoke</span><span class="p">(</span><span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">RUN_PCA</span><span class="p">)</span> <span class="p">}</span> <span class="n">addStyle</span> <span class="s">"standard-button"</span><span class="p">,</span>
        <span class="nf">buttonOf</span><span class="p">(</span><span class="s">"Export Projections"</span><span class="p">)</span> <span class="p">{</span> <span class="n">genOp</span><span class="p">.</span><span class="nf">invoke</span><span class="p">(</span><span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">SAVE_PROJECTIONS</span><span class="p">)</span> <span class="p">}</span> <span class="n">addStyle</span> <span class="s">"standard-button"</span>
    <span class="p">)</span> <span class="n">alignTo</span> <span class="nc">Pos</span><span class="p">.</span><span class="nc">CENTER</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">pcaGridPane</span><span class="p">()</span> <span class="p">=</span> <span class="nc">GridPane</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="n">columnConstraints</span> <span class="p">+=</span> <span class="nc">ColumnConstraints</span><span class="p">(</span><span class="mf">10.0</span><span class="p">,</span> <span class="mf">200.0</span><span class="p">,</span> <span class="mf">288.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span> <span class="n">hgrow</span> <span class="p">=</span> <span class="nc">Priority</span><span class="p">.</span><span class="nc">SOMETIMES</span> <span class="p">}</span>
        <span class="n">columnConstraints</span> <span class="p">+=</span> <span class="nc">ColumnConstraints</span><span class="p">(</span><span class="mf">10.0</span><span class="p">,</span> <span class="mf">200.0</span><span class="p">,</span> <span class="mf">380.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span> <span class="n">hgrow</span> <span class="p">=</span> <span class="nc">Priority</span><span class="p">.</span><span class="nc">SOMETIMES</span> <span class="p">}</span>
        <span class="nf">addRowConstraints</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="n">stdRowConst</span><span class="p">)</span>
        <span class="nf">pcaLeftColumn</span><span class="p">()</span>
        <span class="nf">pcaRightColumn</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nc">GridPane</span><span class="p">.</span><span class="nf">pcaRightColumn</span><span class="p">()</span> <span class="p">{</span>
        <span class="nf">add</span><span class="p">(</span><span class="nf">promptOf</span><span class="p">(</span><span class="s">"Component Analysis Type"</span><span class="p">),</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
        <span class="nf">with</span><span class="p">(</span><span class="nc">ToggleGroup</span><span class="p">())</span> <span class="p">{</span>
            <span class="nf">add</span><span class="p">(</span><span class="nf">radioButtonOf</span><span class="p">(</span><span class="s">"PCA (EigenValue)"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">analysisMethodPca</span><span class="p">,</span> <span class="k">this</span><span class="p">,</span> <span class="s">"std-radio"</span><span class="p">),</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
            <span class="nf">add</span><span class="p">(</span>
                <span class="nf">radioButtonOf</span><span class="p">(</span><span class="s">"Singular Value Decomposition"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">analysisMethodSvd</span><span class="p">,</span> <span class="k">this</span><span class="p">,</span> <span class="s">"std-radio"</span><span class="p">),</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span>
            <span class="p">)</span>
        <span class="p">}</span>
        <span class="nf">add</span><span class="p">(</span><span class="nf">promptOf</span><span class="p">(</span><span class="s">"Input Data Source"</span><span class="p">),</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
        <span class="nf">with</span><span class="p">(</span><span class="nc">ToggleGroup</span><span class="p">())</span> <span class="p">{</span>
            <span class="nf">add</span><span class="p">(</span><span class="nf">radioButtonOf</span><span class="p">(</span><span class="s">"Use Hypersurface"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">dummyBoolean1</span><span class="p">,</span> <span class="k">this</span><span class="p">,</span> <span class="s">"std-radio"</span><span class="p">),</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span>
            <span class="nf">add</span><span class="p">(</span><span class="nf">radioButtonOf</span><span class="p">(</span><span class="s">"Use Hyperspace"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">dummyBoolean2</span><span class="p">,</span> <span class="k">this</span><span class="p">,</span> <span class="s">"std-radio"</span><span class="p">),</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nc">GridPane</span><span class="p">.</span><span class="nf">pcaLeftColumn</span><span class="p">()</span> <span class="p">{</span>
        <span class="nf">stackedIntSpinner</span><span class="p">(</span><span class="s">"Number of Components"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">numberOfPcaComponents</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="n">addStyle</span> <span class="s">"std-spinner"</span>
        <span class="nf">add</span><span class="p">(</span><span class="nf">checkBoxOf</span><span class="p">(</span><span class="s">"Enabled Ranged Fitting (Experimental)"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">rangeFitting</span><span class="p">,</span> <span class="s">"std-checkbox"</span><span class="p">),</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
        <span class="nf">stackedIntSpinner</span><span class="p">(</span><span class="s">"Fit Start Index"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">fitStartIndex</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">500</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">bindDisable</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">rangeFitting</span><span class="p">.</span><span class="nf">not</span><span class="p">())</span> <span class="n">addStyle</span> <span class="s">"std-spinner"</span>
        <span class="nf">stackedIntSpinner</span><span class="p">(</span><span class="s">"Fit End Index"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">fitEndIndex</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">2000</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">bindDisable</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">rangeFitting</span><span class="p">.</span><span class="nf">not</span><span class="p">())</span> <span class="n">addStyle</span> <span class="s">"std-spinner"</span>
        <span class="nf">stackedIntSpinner</span><span class="p">(</span><span class="s">"Output Scaling Factor"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">pcaScalingFactor</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1000</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">7</span><span class="p">)</span> <span class="n">addStyle</span> <span class="s">"std-spinner"</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">distanceLeft</span><span class="p">()</span> <span class="p">=</span> <span class="nc">VBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="n">padding</span> <span class="p">=</span> <span class="nc">Insets</span><span class="p">(</span><span class="mf">25.0</span><span class="p">)</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nf">h3Of</span><span class="p">(</span><span class="s">"Distance Metric"</span><span class="p">)</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nf">dataOf</span><span class="p">(</span><span class="nc">StringExpression</span><span class="p">.</span><span class="nf">stringExpression</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">selectedDistance</span><span class="p">.</span><span class="nf">flatMap</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">metric</span> <span class="p">}</span>
            <span class="p">.</span><span class="nf">orElse</span><span class="p">(</span><span class="s">"Select Distance"</span><span class="p">)))</span>
        <span class="nf">with</span><span class="p">(</span><span class="nc">ToggleGroup</span><span class="p">())</span> <span class="p">{</span>
            <span class="n">children</span> <span class="p">+=</span> <span class="nf">radioButtonOf</span><span class="p">(</span><span class="s">"Point to Point"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">pointToPoint</span><span class="p">,</span> <span class="k">this</span><span class="p">,</span> <span class="s">"std-radio"</span><span class="p">)</span>
            <span class="n">children</span> <span class="p">+=</span> <span class="nf">radioButtonOf</span><span class="p">(</span><span class="s">"Point to Group"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">pointToGroup</span><span class="p">,</span> <span class="k">this</span><span class="p">,</span> <span class="s">"std-radio"</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nf">h3Of</span><span class="p">(</span><span class="s">"Connector Thickness"</span><span class="p">)</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nf">intSpinnerOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">50</span><span class="p">,</span> <span class="s">"standard-spinner"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">connectorThickness</span><span class="p">.</span><span class="nf">asObject</span><span class="p">())</span>
            <span class="p">.</span><span class="nf">setStep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">bindDisable</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">selectedDistance</span><span class="p">.</span><span class="n">isNull</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">withInitialDelay</span><span class="p">(</span><span class="nc">Duration</span><span class="p">.</span><span class="nf">millis</span><span class="p">(</span><span class="mf">500.0</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">withRepeatDelay</span><span class="p">(</span><span class="nc">Duration</span><span class="p">.</span><span class="nf">millis</span><span class="p">(</span><span class="mf">500.0</span><span class="p">))</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nf">promptOf</span><span class="p">(</span><span class="s">"Connector Colour"</span><span class="p">)</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nc">ColorPicker</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
            <span class="n">promptText</span> <span class="p">=</span> <span class="s">"Change the colour of the 3D connector"</span>
            <span class="nf">valueProperty</span><span class="p">().</span><span class="nf">bindBidirectional</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">connectorColour</span><span class="p">)</span>
            <span class="nf">bindDisable</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">selectedDistance</span><span class="p">.</span><span class="n">isNull</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">distanceRight</span><span class="p">()</span> <span class="p">=</span> <span class="nc">VBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nc">HBox</span><span class="p">(</span>
            <span class="mf">10.0</span><span class="p">,</span>
            <span class="nf">h3Of</span><span class="p">(</span><span class="s">"Collected Distances"</span><span class="p">),</span>
            <span class="nf">buttonOf</span><span class="p">(</span><span class="s">"Clear All"</span><span class="p">)</span> <span class="p">{</span> <span class="n">genOp</span><span class="p">.</span><span class="nf">invoke</span><span class="p">(</span><span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">CLEAR_DISTANCES</span><span class="p">)</span> <span class="p">})</span> <span class="n">alignTo</span> <span class="nc">Pos</span><span class="p">.</span><span class="nc">CENTER</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="p">(</span><span class="nc">ListView</span><span class="p">&lt;</span><span class="nc">Distance</span><span class="p">&gt;()</span> <span class="n">withItems</span> <span class="n">model</span><span class="p">.</span><span class="n">distanceList</span> <span class="n">withCellFactory</span> <span class="nc">Callback</span> <span class="p">{</span> <span class="nc">DistanceListCell</span><span class="p">()</span> <span class="p">}</span> <span class="n">connectSelection</span> <span class="n">model</span><span class="p">.</span><span class="n">selectedDistance</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
            <span class="n">model</span><span class="p">.</span><span class="n">externallySelectedDistance</span><span class="p">.</span><span class="nf">subscribe</span> <span class="p">{</span> <span class="n">newValue</span> <span class="p">-&gt;</span>
                <span class="k">this</span><span class="p">.</span><span class="n">selectionModel</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="n">newValue</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">manifoldPropertyBox</span><span class="p">():</span> <span class="nc">Region</span> <span class="p">=</span> <span class="nc">VBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nf">promptOf</span><span class="p">(</span><span class="s">"Selected Manifold Properties"</span><span class="p">)</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nf">titledPaneOf</span><span class="p">(</span><span class="s">"Material"</span><span class="p">)</span> <span class="p">{</span>
            <span class="nc">TwoColumnGridPane</span><span class="p">().</span><span class="nf">addColorPickerRow</span><span class="p">(</span><span class="s">"Diffuse Colour"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">manifoldDiffuseColour</span><span class="p">,</span> <span class="s">"std-color-picker"</span><span class="p">)</span>
                <span class="p">.</span><span class="nf">addColorPickerRow</span><span class="p">(</span><span class="s">"Wire Mesh Colour"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">manifoldWireMeshColour</span><span class="p">,</span> <span class="s">"std-color-picker"</span><span class="p">)</span>
                <span class="p">.</span><span class="nf">addColorPickerRow</span><span class="p">(</span><span class="s">"Specular Colour"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">manifoldSpecularColour</span><span class="p">,</span> <span class="s">"std-color-picker"</span><span class="p">)</span>
        <span class="p">}</span> <span class="n">withCollapsable</span> <span class="k">false</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nf">meshViewPane</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">meshViewPane</span><span class="p">()</span> <span class="p">=</span> <span class="nf">titledPaneOf</span><span class="p">(</span><span class="s">"MeshView"</span><span class="p">)</span> <span class="p">{</span>
        <span class="nc">VBox</span><span class="p">(</span><span class="mf">5.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
            <span class="n">children</span> <span class="p">+=</span> <span class="nf">radioButtonHBox</span><span class="p">(</span>
                <span class="s">"Cull Face"</span><span class="p">,</span>
                <span class="nf">listOf</span><span class="p">(</span>
                    <span class="nc">Pair</span><span class="p">(</span><span class="s">"Front"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">frontCullFace</span><span class="p">),</span>
                    <span class="nc">Pair</span><span class="p">(</span><span class="s">"Back"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">backCullFace</span><span class="p">),</span>
                    <span class="nc">Pair</span><span class="p">(</span><span class="s">"None"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">noneCullFace</span><span class="p">)</span>
                <span class="p">),</span> <span class="mf">5.0</span><span class="p">,</span> <span class="s">"std-radio-button"</span>
            <span class="p">)</span>
            <span class="n">children</span> <span class="p">+=</span> <span class="nf">radioButtonHBox</span><span class="p">(</span>
                <span class="s">"Draw Mode"</span><span class="p">,</span>
                <span class="nf">listOf</span><span class="p">(</span><span class="nc">Pair</span><span class="p">(</span><span class="s">"Fill"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">fillDrawMode</span><span class="p">),</span> <span class="nc">Pair</span><span class="p">(</span><span class="s">"Lines"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">linesDrawMode</span><span class="p">)),</span>
                <span class="mf">5.0</span><span class="p">,</span>
                <span class="s">"std-radio-button"</span>
            <span class="p">)</span>
            <span class="n">children</span> <span class="p">+=</span> <span class="nc">HBox</span><span class="p">(</span>
                <span class="mf">10.0</span><span class="p">,</span>
                <span class="nf">checkBoxOf</span><span class="p">(</span><span class="s">"Show Wire Frame"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">showWireFrame</span><span class="p">,</span> <span class="s">"std-checkbox"</span><span class="p">),</span>
                <span class="nf">checkBoxOf</span><span class="p">(</span><span class="s">"Show Control Points"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">showControlPoints</span><span class="p">,</span> <span class="s">"std-checkbox"</span><span class="p">)</span>
            <span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span> <span class="n">withCollapsable</span> <span class="k">false</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">hullTop</span><span class="p">():</span> <span class="nc">Region</span> <span class="p">=</span> <span class="nc">HBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="nf">padWith</span><span class="p">(</span><span class="mf">10.0</span><span class="p">)</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nf">hullTopGridPane</span><span class="p">()</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nc">VBox</span><span class="p">(</span>
            <span class="mf">20.0</span><span class="p">,</span>
            <span class="nf">buttonOf</span><span class="p">(</span><span class="s">"Generate"</span><span class="p">)</span> <span class="p">{</span> <span class="n">genOp</span><span class="p">.</span><span class="nf">invoke</span><span class="p">(</span><span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">GENERATE</span><span class="p">)</span> <span class="p">},</span>
            <span class="nf">buttonOf</span><span class="p">(</span><span class="s">"Cluster Tools"</span><span class="p">)</span> <span class="p">{</span> <span class="n">genOp</span><span class="p">.</span><span class="nf">invoke</span><span class="p">(</span><span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">BUILD_CLUSTER</span><span class="p">)</span> <span class="p">}</span>
        <span class="p">)</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">hullTopGridPane</span><span class="p">()</span> <span class="p">=</span> <span class="nc">TwoColumnGridPane</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">addRadioButtonHBoxRow</span><span class="p">(</span>
            <span class="s">"Point Set"</span><span class="p">,</span>
            <span class="nf">listOf</span><span class="p">(</span><span class="nc">Pair</span><span class="p">(</span><span class="s">"Visible"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">useVisible</span><span class="p">),</span> <span class="nc">Pair</span><span class="p">(</span><span class="s">"All"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">useAll</span><span class="p">)),</span>
            <span class="mf">5.0</span><span class="p">,</span>
            <span class="s">"std-radio-button"</span>
        <span class="p">)</span>
        <span class="p">.</span><span class="nf">addRow</span><span class="p">(</span><span class="s">"Distance Tolerance"</span><span class="p">)</span> <span class="p">{</span>
            <span class="nc">HBox</span><span class="p">(</span>
                <span class="mf">10.0</span><span class="p">,</span>
                <span class="nf">checkBoxOf</span><span class="p">(</span><span class="s">"Auto"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">toleranceAuto</span><span class="p">,</span> <span class="s">"std-checkbox"</span><span class="p">),</span>
                <span class="nf">doubleSpinnerOf</span><span class="p">(</span><span class="mf">0.1</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="s">"std-spinner"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">toleranceManual</span><span class="p">.</span><span class="nf">asObject</span><span class="p">()).</span><span class="nf">setStep</span><span class="p">(</span><span class="mf">0.1</span><span class="p">)</span>
                    <span class="p">.</span><span class="nf">bindDisable</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">toleranceAuto</span><span class="p">)</span>
            <span class="p">)</span>
        <span class="p">}</span>
        <span class="p">.</span><span class="nf">addChoiceBoxRow</span><span class="p">(</span><span class="s">"Find by Label"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">factorLabelList</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">selectedFactorLabel</span><span class="p">,</span> <span class="s">"std-choice-box"</span><span class="p">,</span> <span class="k">true</span><span class="p">)</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">manifoldBox</span><span class="p">():</span> <span class="nc">Region</span> <span class="p">=</span> <span class="nc">VBox</span><span class="p">(</span><span class="mf">5.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nf">h3Of</span><span class="p">(</span><span class="s">"Generated Manifolds"</span><span class="p">)</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nc">HBox</span><span class="p">(</span><span class="mf">20.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
            <span class="n">children</span> <span class="p">+=</span> <span class="nf">buttonOf</span><span class="p">(</span><span class="s">"Clear All"</span><span class="p">)</span> <span class="p">{</span> <span class="n">genOp</span><span class="p">.</span><span class="nf">invoke</span><span class="p">(</span><span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">CLEAR_ALL</span><span class="p">)</span> <span class="p">}</span>
            <span class="n">children</span> <span class="p">+=</span> <span class="nf">buttonOf</span><span class="p">(</span><span class="s">"Export All"</span><span class="p">)</span> <span class="p">{</span> <span class="n">genOp</span><span class="p">.</span><span class="nf">invoke</span><span class="p">(</span><span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">EXPORT_ALL</span><span class="p">)</span> <span class="p">}</span>
        <span class="p">}</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nc">ListView</span><span class="p">&lt;</span><span class="nc">Manifold</span><span class="p">&gt;()</span> <span class="n">withItems</span> <span class="n">model</span><span class="p">.</span><span class="n">manifoldList</span> <span class="n">withCellFactory</span> <span class="nc">Callback</span> <span class="p">{</span> <span class="nc">ManifoldListCell</span><span class="p">()</span> <span class="p">}</span> <span class="n">connectSelection</span> <span class="n">model</span><span class="p">.</span><span class="n">selectedManifold</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>One of the first things you should notice is that except for the container classes, none of the <code class="language-plaintext highlighter-rouge">Nodes</code> are instantiated directly using their constructors.  All of them are instantiated via builder methods of some sort, and those builders are generic enough that they are included in <code class="language-plaintext highlighter-rouge">WidgetsFX</code>.</p>

<div class="notice--primary">
 <img src="/assets/logos/brain.png" alt="primary" style="float:left;margin-right: 10px" />
 <p style="overflow:auto; float:none">
   This points out one of the biggest problems with the standard JavaFX library - a lack of constructors that allow a parameter for value binding.
 </p>
</div>

<p>For those layout classes, I’ve used three standard techniques for populating them:</p>

<ol>
  <li>Providing the children as constructor parameters.</li>
  <li>Using <code class="language-plaintext highlighter-rouge">getChildren().add()</code> via <code class="language-plaintext highlighter-rouge">children += </code> inside <code class="language-plaintext highlighter-rouge">.apply{}</code>.</li>
  <li>Using the extension function <code class="language-plaintext highlighter-rouge">Pane.addChild()</code></li>
</ol>

<p>In practice, I found that <code class="language-plaintext highlighter-rouge">Pane.addChild()</code> outside of <code class="language-plaintext highlighter-rouge">apply{}</code> was no better than just including the children as constructor parameters.  It’s also not clear if <code class="language-plaintext highlighter-rouge">Pane.addChild()</code> is any clearer than <code class="language-plaintext highlighter-rouge">children += </code> inside of an <code class="language-plaintext highlighter-rouge">apply{}</code> block.</p>

<p>There are a fair number of <code class="language-plaintext highlighter-rouge">GridPanes</code> in this layout.  <code class="language-plaintext highlighter-rouge">GridPane</code> is fine when there is a strict need to keep columns and rows locked together in some fashion, but that is rarely the case in this layout.  Particularly in the UMAP <code class="language-plaintext highlighter-rouge">GridPane</code>, where the <code class="language-plaintext highlighter-rouge">Labels</code> and <code class="language-plaintext highlighter-rouge">Controls</code> are stacked in successive rows, with the <code class="language-plaintext highlighter-rouge">Spinner</code> inputs in one column and the <code class="language-plaintext highlighter-rouge">Slider</code> inputs in another column.  Is there really any need to keep the elements aligned by row?</p>

<p>While I don’t think I would use a <code class="language-plaintext highlighter-rouge">GridPane</code> in this case (two <code class="language-plaintext highlighter-rouge">VBoxes</code> in an <code class="language-plaintext highlighter-rouge">HBox</code> would be simpler), I did create the extension functions <code class="language-plaintext highlighter-rouge">GridPane.stackedSlider</code>, and <code class="language-plaintext highlighter-rouge">GridPane.stackedIntSpinner</code> to get the repeated elements out of the <code class="language-plaintext highlighter-rouge">GridPane</code> configuration.</p>

<p>For the <code class="language-plaintext highlighter-rouge">infix</code> decorator functions, I’ve used them as <code class="language-plaintext highlighter-rouge">infix</code> when only one or two functions were called, and they would fit onto a single line.  When more functions were called, it was more clear to use the regular notation and stack them one per line in the code.</p>

<p>I am aware that the infix notation and the extension functions are difficult to get used to at first.  A couple of years ago, I would have shied away from using them and simply put all of this functionality into <code class="language-plaintext highlighter-rouge">apply{}</code> blocks.  Today, I find the <code class="language-plaintext highlighter-rouge">apply{}</code> approach to be overly verbose in many cases.</p>

<p>The net result of the extension functions and builder methods is to strip virtually all of the configuration details and boilerplate out of the layout code leaving something where you can understand the effect of that configuration without obscuring the layout itself.</p>

<p>I do feel that this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">fun</span> <span class="nf">manifoldPropertyBox</span><span class="p">():</span> <span class="nc">Region</span> <span class="p">=</span> <span class="nc">VBox</span><span class="p">(</span><span class="mf">10.0</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
    <span class="n">children</span> <span class="p">+=</span> <span class="nf">promptOf</span><span class="p">(</span><span class="s">"Selected Manifold Properties"</span><span class="p">)</span>
    <span class="n">children</span> <span class="p">+=</span> <span class="nf">titledPaneOf</span><span class="p">(</span><span class="s">"Material"</span><span class="p">)</span> <span class="p">{</span>
        <span class="nc">TwoColumnGridPane</span><span class="p">()</span>
            <span class="p">.</span><span class="nf">addColorPickerRow</span><span class="p">(</span><span class="s">"Diffuse Colour"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">manifoldDiffuseColour</span><span class="p">,</span> <span class="s">"std-color-picker"</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">addColorPickerRow</span><span class="p">(</span><span class="s">"Wire Mesh Colour"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">manifoldWireMeshColour</span><span class="p">,</span> <span class="s">"std-color-picker"</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">addColorPickerRow</span><span class="p">(</span><span class="s">"Specular Colour"</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">manifoldSpecularColour</span><span class="p">,</span> <span class="s">"std-color-picker"</span><span class="p">)</span>
    <span class="p">}</span> <span class="n">withCollapsable</span> <span class="k">false</span>
    <span class="n">children</span> <span class="p">+=</span> <span class="nf">meshViewPane</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>
<p>is far easier to understand than:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;VBox</span> <span class="na">spacing=</span><span class="s">"10.0"</span> <span class="na">BorderPane.alignment=</span><span class="s">"CENTER"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;children&gt;</span>
    <span class="nt">&lt;Label</span> <span class="na">text=</span><span class="s">"Selected Manifold Properties"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;TitledPane</span> <span class="na">collapsible=</span><span class="s">"false"</span> <span class="na">text=</span><span class="s">"Material"</span> <span class="na">VBox.vgrow=</span><span class="s">"ALWAYS"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;content&gt;</span>
              <span class="nt">&lt;VBox</span> <span class="na">spacing=</span><span class="s">"5.0"</span><span class="nt">&gt;</span>
                  <span class="nt">&lt;children&gt;</span>
                      <span class="nt">&lt;HBox</span> <span class="na">alignment=</span><span class="s">"CENTER_LEFT"</span> <span class="na">spacing=</span><span class="s">"10.0"</span><span class="nt">&gt;</span>
                          <span class="nt">&lt;children&gt;</span>
                                <span class="nt">&lt;Label</span> <span class="na">prefWidth=</span><span class="s">"125.0"</span> <span class="na">text=</span><span class="s">"Diffuse Color"</span><span class="nt">/&gt;</span>
                                <span class="nt">&lt;ColorPicker</span> <span class="na">fx:id=</span><span class="s">"manifoldDiffuseColorPicker"</span> <span class="na">editable=</span><span class="s">"true"</span> <span class="na">prefHeight=</span><span class="s">"50.0"</span>
                                             <span class="na">prefWidth=</span><span class="s">"150.0"</span><span class="nt">/&gt;</span>
                          <span class="nt">&lt;/children&gt;</span>
                    <span class="nt">&lt;/HBox&gt;</span>
                    <span class="nt">&lt;HBox</span> <span class="na">alignment=</span><span class="s">"CENTER_LEFT"</span> <span class="na">spacing=</span><span class="s">"10.0"</span><span class="nt">&gt;</span>
                          <span class="nt">&lt;children&gt;</span>
                                <span class="nt">&lt;Label</span> <span class="na">prefWidth=</span><span class="s">"125.0"</span> <span class="na">text=</span><span class="s">"Wire Mesh Color"</span><span class="nt">/&gt;</span>
                                <span class="nt">&lt;ColorPicker</span> <span class="na">fx:id=</span><span class="s">"manifoldWireMeshColorPicker"</span> <span class="na">editable=</span><span class="s">"true"</span> <span class="na">prefHeight=</span><span class="s">"50.0"</span>
                                                 <span class="na">prefWidth=</span><span class="s">"150.0"</span><span class="nt">/&gt;</span>
                         <span class="nt">&lt;/children&gt;</span>
                    <span class="nt">&lt;/HBox&gt;</span>
                    <span class="nt">&lt;HBox</span> <span class="na">alignment=</span><span class="s">"CENTER_LEFT"</span> <span class="na">spacing=</span><span class="s">"10.0"</span><span class="nt">&gt;</span>
                         <span class="nt">&lt;children&gt;</span>
                              <span class="nt">&lt;Label</span> <span class="na">prefWidth=</span><span class="s">"125.0"</span> <span class="na">text=</span><span class="s">"Specular Color"</span><span class="nt">/&gt;</span>
                             <span class="nt">&lt;ColorPicker</span> <span class="na">fx:id=</span><span class="s">"manifoldSpecularColorPicker"</span> <span class="na">editable=</span><span class="s">"true"</span> <span class="na">prefHeight=</span><span class="s">"50.0"</span>
                                     <span class="na">prefWidth=</span><span class="s">"150.0"</span><span class="nt">/&gt;</span>
                         <span class="nt">&lt;/children&gt;</span>
                    <span class="nt">&lt;/HBox&gt;</span>
                 <span class="nt">&lt;/children&gt;</span>
             <span class="nt">&lt;/VBox&gt;</span>
       <span class="nt">&lt;/content&gt;</span>
   <span class="nt">&lt;/TitledPane&gt;</span>
  .
  .
  .
<span class="nt">&lt;/VBox&gt;</span>   
</code></pre></div></div>
<p>Especially when you take into consideration the ~40 lines of code that configure these <code class="language-plaintext highlighter-rouge">ColorPickers</code> in the FXML Controller.  In the Kotlin code, these 3 lines completely configue the <code class="language-plaintext highlighter-rouge">ColorPickers</code> and they are never referenced again…anywhere.</p>

<h3 id="listview-cells">ListView Cells</h3>

<p>The original design didn’t properly handle the two <code class="language-plaintext highlighter-rouge">ListViews</code> properly at all.  These two classes provide <code class="language-plaintext highlighter-rouge">Cell</code> layouts that emulate what the original code did:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">DistanceListCell</span> <span class="p">:</span> <span class="nc">ListCell</span><span class="p">&lt;</span><span class="nc">Distance</span><span class="p">&gt;()</span> <span class="p">{</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">label</span> <span class="p">=</span> <span class="nc">Label</span><span class="p">()</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">distanceValueLabel</span> <span class="p">=</span> <span class="nc">Label</span><span class="p">()</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">visibleCB</span> <span class="p">=</span> <span class="nc">CheckBox</span><span class="p">(</span><span class="s">"Visible"</span><span class="p">)</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">layout</span> <span class="p">=</span> <span class="nc">HBox</span><span class="p">(</span><span class="mf">5.0</span><span class="p">,</span> <span class="n">visibleCB</span><span class="p">,</span> <span class="n">label</span><span class="p">,</span> <span class="n">distanceValueLabel</span><span class="p">)</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">updateItem</span><span class="p">(</span><span class="n">newItem</span><span class="p">:</span> <span class="nc">Distance</span><span class="p">?,</span> <span class="n">isEmpty</span><span class="p">:</span> <span class="nc">Boolean</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">item</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span>
            <span class="n">label</span><span class="p">.</span><span class="nf">textProperty</span><span class="p">().</span><span class="nf">unbind</span><span class="p">()</span>
            <span class="n">distanceValueLabel</span><span class="p">.</span><span class="nf">textProperty</span><span class="p">().</span><span class="nf">unbind</span><span class="p">()</span>
            <span class="n">visibleCB</span><span class="p">.</span><span class="nf">selectedProperty</span><span class="p">().</span><span class="nf">unbindBidirectional</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">visible</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="k">super</span><span class="p">.</span><span class="nf">updateItem</span><span class="p">(</span><span class="n">newItem</span><span class="p">,</span> <span class="n">isEmpty</span><span class="p">)</span>
        <span class="n">graphic</span> <span class="p">=</span> <span class="k">null</span>
        <span class="n">text</span> <span class="p">=</span> <span class="k">null</span>
        <span class="k">if</span> <span class="p">(!</span><span class="n">isEmpty</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">newItem</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span>
                <span class="n">label</span><span class="p">.</span><span class="nf">textProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">label</span><span class="p">)</span>
                <span class="n">visibleCB</span><span class="p">.</span><span class="nf">selectedProperty</span><span class="p">().</span><span class="nf">bindBidirectional</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">visible</span><span class="p">)</span>
                <span class="n">distanceValueLabel</span><span class="p">.</span><span class="nf">textProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="nc">Bindings</span><span class="p">.</span><span class="nf">concat</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">metric</span><span class="p">,</span> <span class="s">": "</span><span class="p">,</span> <span class="n">it</span><span class="p">.</span><span class="n">distance</span><span class="p">.</span><span class="nf">asString</span><span class="p">()))</span>
                <span class="n">graphic</span> <span class="p">=</span> <span class="n">layout</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">ManifoldListCell</span> <span class="p">:</span> <span class="nc">ListCell</span><span class="p">&lt;</span><span class="nc">Manifold</span><span class="p">&gt;()</span> <span class="p">{</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">label</span> <span class="p">=</span> <span class="nc">TextField</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="nf">focusedProperty</span><span class="p">().</span><span class="nf">subscribe</span> <span class="p">{</span> <span class="n">newVal</span> <span class="p">-&gt;</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">newVal</span><span class="p">)</span> <span class="n">listView</span><span class="p">.</span><span class="n">selectionModel</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="n">item</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">visibleCB</span> <span class="p">=</span> <span class="nc">CheckBox</span><span class="p">(</span><span class="s">"Visible"</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="nf">focusedProperty</span><span class="p">().</span><span class="nf">subscribe</span> <span class="p">{</span> <span class="n">newVal</span> <span class="p">-&gt;</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">newVal</span><span class="p">)</span> <span class="n">listView</span><span class="p">.</span><span class="n">selectionModel</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="n">item</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">layout</span> <span class="p">=</span> <span class="nc">HBox</span><span class="p">(</span><span class="mf">5.0</span><span class="p">,</span> <span class="n">visibleCB</span><span class="p">,</span> <span class="n">label</span><span class="p">)</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">updateItem</span><span class="p">(</span><span class="n">newItem</span><span class="p">:</span> <span class="nc">Manifold</span><span class="p">?,</span> <span class="n">isEmpty</span><span class="p">:</span> <span class="nc">Boolean</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">item</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span>
            <span class="n">label</span><span class="p">.</span><span class="nf">textProperty</span><span class="p">().</span><span class="nf">unbindBidirectional</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">label</span><span class="p">)</span>
            <span class="n">visibleCB</span><span class="p">.</span><span class="nf">selectedProperty</span><span class="p">().</span><span class="nf">unbindBidirectional</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">visible</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="k">super</span><span class="p">.</span><span class="nf">updateItem</span><span class="p">(</span><span class="n">newItem</span><span class="p">,</span> <span class="n">isEmpty</span><span class="p">)</span>
        <span class="n">graphic</span> <span class="p">=</span> <span class="k">null</span>
        <span class="n">text</span> <span class="p">=</span> <span class="k">null</span>
        <span class="k">if</span> <span class="p">(!</span><span class="n">isEmpty</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">newItem</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span>
                <span class="n">label</span><span class="p">.</span><span class="nf">textProperty</span><span class="p">().</span><span class="nf">bindBidirectional</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">label</span><span class="p">)</span>
                <span class="n">visibleCB</span><span class="p">.</span><span class="nf">selectedProperty</span><span class="p">().</span><span class="nf">bindBidirectional</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">visible</span><span class="p">)</span>
                <span class="n">graphic</span> <span class="p">=</span> <span class="n">layout</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="the-controller">The Controller</h2>

<p>Here’s the code for the Controller:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">FromFxmlController</span><span class="p">(</span><span class="n">sharedElements</span><span class="p">:</span> <span class="nc">SharedElements</span><span class="p">,</span> <span class="n">externalFunctions</span><span class="p">:</span> <span class="nc">SharedFunctions</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">model</span> <span class="p">=</span> <span class="nc">FromFxmlModel</span><span class="p">(</span><span class="n">sharedElements</span><span class="p">)</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">viewBuilder</span> <span class="p">=</span> <span class="nc">FromFxmlViewBuilder</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="k">this</span><span class="o">::</span><span class="n">dataOperation</span><span class="p">,</span> <span class="k">this</span><span class="o">::</span><span class="n">generalOperation</span><span class="p">)</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">interactor</span> <span class="p">=</span> <span class="nc">FromFxmlInteractor</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">externalFunctions</span><span class="p">)</span>

    <span class="k">fun</span> <span class="nf">getView</span><span class="p">():</span> <span class="nc">Region</span> <span class="p">=</span> <span class="n">viewBuilder</span><span class="p">.</span><span class="nf">build</span><span class="p">()</span> <span class="nf">asFileDrop</span> <span class="p">{</span>
        <span class="nf">runStandardVoidTask</span><span class="p">({</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">loadUmap</span><span class="p">(</span><span class="n">it</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="p">},</span> <span class="p">{</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">completeLoadUmap</span><span class="p">()</span> <span class="p">})</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">dataOperation</span><span class="p">(</span><span class="n">window</span><span class="p">:</span> <span class="nc">Window</span><span class="p">,</span> <span class="n">operation</span><span class="p">:</span> <span class="nc">FileOperation</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">when</span> <span class="p">(</span><span class="n">operation</span><span class="p">)</span> <span class="p">{</span>
            <span class="nc">FileOperation</span><span class="p">.</span><span class="nc">UMAP_SAVE</span> <span class="p">-&gt;</span> <span class="nf">saveUmap</span><span class="p">(</span><span class="n">window</span><span class="p">)</span>
            <span class="nc">FileOperation</span><span class="p">.</span><span class="nc">UMAP_LOAD</span> <span class="p">-&gt;</span> <span class="nf">chooseAndloadUmap</span><span class="p">(</span><span class="n">window</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">generalOperation</span><span class="p">(</span><span class="n">operation</span><span class="p">:</span> <span class="nc">GeneralOperation</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">when</span> <span class="p">(</span><span class="n">operation</span><span class="p">)</span> <span class="p">{</span>
            <span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">BUILD_CLUSTER</span> <span class="p">-&gt;</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">buildCluster</span><span class="p">()</span>
            <span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">GENERATE</span> <span class="p">-&gt;</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">generate</span><span class="p">()</span>
            <span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">CLEAR_ALL</span> <span class="p">-&gt;</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">clearAll</span><span class="p">()</span>
            <span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">EXPORT_ALL</span> <span class="p">-&gt;</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">exportAll</span><span class="p">()</span>
            <span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">CLEAR_DISTANCES</span> <span class="p">-&gt;</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">clearDistances</span><span class="p">()</span>
            <span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">PROJECT</span> <span class="p">-&gt;</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">project</span><span class="p">()</span>
            <span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">EXPORT_MATRIX</span> <span class="p">-&gt;</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">exportMatrix</span><span class="p">()</span>
            <span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">SAVE_PROJECTIONS</span> <span class="p">-&gt;</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">saveProjections</span><span class="p">()</span>
            <span class="nc">GeneralOperation</span><span class="p">.</span><span class="nc">RUN_PCA</span> <span class="p">-&gt;</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">runPca</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">saveUmap</span><span class="p">(</span><span class="n">window</span><span class="p">:</span> <span class="nc">Window</span><span class="p">)</span> <span class="p">{</span>
        <span class="nc">FileChooser</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
            <span class="n">title</span> <span class="p">=</span> <span class="s">"Choose UMAP Config file output.."</span>
            <span class="n">initialFileName</span> <span class="p">=</span> <span class="s">"UmapConfig.json"</span>
            <span class="n">initialDirectory</span> <span class="p">=</span> <span class="n">model</span><span class="p">.</span><span class="n">latestDir</span><span class="p">.</span><span class="n">value</span> <span class="o">?:</span> <span class="nc">File</span><span class="p">(</span><span class="s">"."</span><span class="p">)</span>
        <span class="p">}.</span><span class="nf">showSaveDialog</span><span class="p">(</span><span class="n">window</span><span class="p">)</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span>
            <span class="nf">runStandardVoidTask</span><span class="p">({</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">saveUmap</span><span class="p">(</span><span class="n">it</span><span class="p">)</span> <span class="p">},</span> <span class="p">{</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">completeSaveUmap</span><span class="p">()</span> <span class="p">})</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">chooseAndloadUmap</span><span class="p">(</span><span class="n">window</span><span class="p">:</span> <span class="nc">Window</span><span class="p">)</span> <span class="p">{</span>
        <span class="nc">FileChooser</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
            <span class="n">title</span> <span class="p">=</span> <span class="s">"Choose UMAP Config to load..."</span>
            <span class="n">initialDirectory</span> <span class="p">=</span> <span class="n">model</span><span class="p">.</span><span class="n">latestDir</span><span class="p">.</span><span class="n">value</span> <span class="o">?:</span> <span class="nc">File</span><span class="p">(</span><span class="s">"."</span><span class="p">)</span>
        <span class="p">}.</span><span class="nf">showOpenDialog</span><span class="p">(</span><span class="n">window</span><span class="p">)</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span>
            <span class="nf">runStandardVoidTask</span><span class="p">({</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">loadUmap</span><span class="p">(</span><span class="n">it</span><span class="p">)</span> <span class="p">},</span> <span class="p">{</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">completeLoadUmap</span><span class="p">()</span> <span class="p">})</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This is pretty simple.  There’s the standard instantiation of the other elements, and then two dispacth methods to handle actions triggered by the View.  Additionally, we have two methods to invoke <code class="language-plaintext highlighter-rouge">FileChooser</code> as part of a workflow to handle the file operations.</p>

<h2 id="the-model">The Model</h2>

<p>Here is the Model code:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">FromFxmlModel</span><span class="p">(</span><span class="n">sharedElements</span><span class="p">:</span> <span class="nc">SharedElements</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">externallySelectedDistance</span><span class="p">:</span> <span class="nc">ObjectProperty</span><span class="p">&lt;</span><span class="nc">Distance</span><span class="p">&gt;</span> <span class="p">=</span> <span class="n">sharedElements</span><span class="p">.</span><span class="n">selectedDistance</span>
    <span class="kd">val</span> <span class="py">externallySelectedManifold</span><span class="p">:</span> <span class="nc">ObjectProperty</span><span class="p">&lt;</span><span class="nc">Manifold</span><span class="p">&gt;</span> <span class="p">=</span> <span class="n">sharedElements</span><span class="p">.</span><span class="n">selectedManifold</span>
    <span class="kd">val</span> <span class="py">toleranceManual</span><span class="p">:</span> <span class="nc">DoubleProperty</span> <span class="p">=</span> <span class="n">sharedElements</span><span class="p">.</span><span class="n">toleranceManual</span>

    <span class="kd">val</span> <span class="py">numberOfComponents</span><span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">numberOfEpochs</span><span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="mi">200</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">nearestNeighbour</span><span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="mi">15</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">negativeSampleRate</span><span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">localConnectivity</span><span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">repulsionStr</span><span class="p">:</span> <span class="nc">DoubleProperty</span> <span class="p">=</span> <span class="nc">SimpleDoubleProperty</span><span class="p">(</span><span class="mf">1.0</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">spread</span><span class="p">:</span> <span class="nc">DoubleProperty</span> <span class="p">=</span> <span class="nc">SimpleDoubleProperty</span><span class="p">(</span><span class="mf">1.0</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">minimumDistance</span><span class="p">:</span> <span class="nc">DoubleProperty</span> <span class="p">=</span> <span class="nc">SimpleDoubleProperty</span><span class="p">(</span><span class="mf">0.1</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">opMixRatio</span><span class="p">:</span> <span class="nc">DoubleProperty</span> <span class="p">=</span> <span class="nc">SimpleDoubleProperty</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">targetWeight</span><span class="p">:</span> <span class="nc">DoubleProperty</span> <span class="p">=</span> <span class="nc">SimpleDoubleProperty</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">threshold</span><span class="p">:</span> <span class="nc">DoubleProperty</span> <span class="p">=</span> <span class="nc">SimpleDoubleProperty</span><span class="p">(</span><span class="mf">0.1</span><span class="p">)</span>

    <span class="kd">val</span> <span class="py">numberOfPcaComponents</span><span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">()</span>
    <span class="kd">val</span> <span class="py">dummyBoolean1</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="nc">SimpleBooleanProperty</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">dummyBoolean2</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="nc">SimpleBooleanProperty</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">pcaScalingFactor</span><span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">fitStartIndex</span><span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">fitEndIndex</span><span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="mi">50</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">rangeFitting</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="nc">SimpleBooleanProperty</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">dummyBoolean4</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="nc">SimpleBooleanProperty</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">analysisMethodSvd</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="nc">SimpleBooleanProperty</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">analysisMethodPca</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="nc">SimpleBooleanProperty</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>

    <span class="kd">val</span> <span class="py">connectorThickness</span><span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="mi">17</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">connectorColour</span><span class="p">:</span> <span class="nc">ObjectProperty</span><span class="p">&lt;</span><span class="nc">Color</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">SimpleObjectProperty</span><span class="p">()</span>
    <span class="kd">val</span> <span class="py">pointToPoint</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="nc">SimpleBooleanProperty</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">pointToGroup</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="nc">SimpleBooleanProperty</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>

    <span class="kd">val</span> <span class="py">latestDir</span><span class="p">:</span> <span class="nc">ObjectProperty</span><span class="p">&lt;</span><span class="nc">File</span><span class="p">?&gt;</span> <span class="p">=</span> <span class="nc">SimpleObjectProperty</span><span class="p">()</span>
    <span class="kd">val</span> <span class="py">manifoldDiffuseColour</span><span class="p">:</span> <span class="nc">ObjectProperty</span><span class="p">&lt;</span><span class="nc">Color</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">SimpleObjectProperty</span><span class="p">(</span><span class="nc">Color</span><span class="p">.</span><span class="nc">CYAN</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">manifoldWireMeshColour</span><span class="p">:</span> <span class="nc">ObjectProperty</span><span class="p">&lt;</span><span class="nc">Color</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">SimpleObjectProperty</span><span class="p">(</span><span class="nc">Color</span><span class="p">.</span><span class="nc">BLACK</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">manifoldSpecularColour</span><span class="p">:</span> <span class="nc">ObjectProperty</span><span class="p">&lt;</span><span class="nc">Color</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">SimpleObjectProperty</span><span class="p">(</span><span class="nc">Color</span><span class="p">.</span><span class="nc">BLACK</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">frontCullFace</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="nc">SimpleBooleanProperty</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">backCullFace</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="nc">SimpleBooleanProperty</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">noneCullFace</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="nc">SimpleBooleanProperty</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">fillDrawMode</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="nc">SimpleBooleanProperty</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">linesDrawMode</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="nc">SimpleBooleanProperty</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">showWireFrame</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="n">sharedElements</span><span class="p">.</span><span class="n">showWireFrame</span>
    <span class="kd">val</span> <span class="py">showControlPoints</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="n">sharedElements</span><span class="p">.</span><span class="n">showControlPoints</span>
    <span class="kd">val</span> <span class="py">useAll</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="n">sharedElements</span><span class="p">.</span><span class="n">useAll</span>
    <span class="kd">val</span> <span class="py">useVisible</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="n">sharedElements</span><span class="p">.</span><span class="n">useVisible</span>
    <span class="kd">val</span> <span class="py">toleranceAuto</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="nc">SimpleBooleanProperty</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>

    <span class="kd">val</span> <span class="py">distanceList</span><span class="p">:</span> <span class="nc">ObservableList</span><span class="p">&lt;</span><span class="nc">Distance</span><span class="p">&gt;</span> <span class="p">=</span> <span class="n">sharedElements</span><span class="p">.</span><span class="n">distanceList</span>
    <span class="kd">val</span> <span class="py">manifoldList</span><span class="p">:</span> <span class="nc">ObservableList</span><span class="p">&lt;</span><span class="nc">Manifold</span><span class="p">&gt;</span> <span class="p">=</span> <span class="n">sharedElements</span><span class="p">.</span><span class="n">manifoldList</span>
    <span class="kd">val</span> <span class="py">factorLabelList</span><span class="p">:</span> <span class="nc">ObservableList</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">FXCollections</span><span class="p">.</span><span class="nf">observableArrayList</span><span class="p">()</span>
    <span class="kd">val</span> <span class="py">selectedFactorLabel</span> <span class="p">=</span> <span class="n">sharedElements</span><span class="p">.</span><span class="n">selectedFactorLabel</span>
    <span class="kd">val</span> <span class="py">selectedDistance</span><span class="p">:</span> <span class="nc">ObjectProperty</span><span class="p">&lt;</span><span class="nc">Distance</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">SimpleObjectProperty</span><span class="p">()</span>
    <span class="kd">val</span> <span class="py">selectedManifold</span><span class="p">:</span> <span class="nc">ObjectProperty</span><span class="p">&lt;</span><span class="nc">Manifold</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">SimpleObjectProperty</span><span class="p">()</span>

    <span class="nf">init</span> <span class="p">{</span>
        <span class="n">sharedElements</span><span class="p">.</span><span class="n">manifoldCullFace</span><span class="p">.</span><span class="nf">bind</span><span class="p">(</span><span class="nc">Bindings</span><span class="p">.</span><span class="nf">createObjectBinding</span><span class="p">({</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">frontCullFace</span><span class="p">.</span><span class="n">value</span><span class="p">)</span> <span class="k">return</span><span class="nd">@createObjectBinding</span> <span class="nc">CullFace</span><span class="p">.</span><span class="nc">FRONT</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">backCullFace</span><span class="p">.</span><span class="n">value</span><span class="p">)</span> <span class="k">return</span><span class="nd">@createObjectBinding</span> <span class="nc">CullFace</span><span class="p">.</span><span class="nc">BACK</span>
            <span class="k">return</span><span class="nd">@createObjectBinding</span> <span class="nc">CullFace</span><span class="p">.</span><span class="nc">NONE</span>
        <span class="p">},</span> <span class="n">frontCullFace</span><span class="p">,</span> <span class="n">backCullFace</span><span class="p">,</span> <span class="n">noneCullFace</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This is just a POJO of JavaFX <code class="language-plaintext highlighter-rouge">Observable</code> classes.  The fields that are tied to the external application are instantiated as references to the corresponding field in <code class="language-plaintext highlighter-rouge">SharedElements</code>.  <code class="language-plaintext highlighter-rouge">SharedElements.manifoldCullFace</code> corresponds to whichever of three <code class="language-plaintext highlighter-rouge">BooleanProperties</code> is <code class="language-plaintext highlighter-rouge">true</code>, and is bound that way.</p>

<p><code class="language-plaintext highlighter-rouge">SharedElements</code> is not exposed to any other component of the MVCI construct, and looks like this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">SharedElements</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">distanceList</span><span class="p">:</span> <span class="nc">ObservableList</span><span class="p">&lt;</span><span class="nc">Distance</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">FXCollections</span><span class="p">.</span><span class="nf">observableArrayList</span><span class="p">()</span>
    <span class="kd">val</span> <span class="py">manifoldList</span><span class="p">:</span> <span class="nc">ObservableList</span><span class="p">&lt;</span><span class="nc">Manifold</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">FXCollections</span><span class="p">.</span><span class="nf">observableArrayList</span><span class="p">()</span>
    <span class="kd">val</span> <span class="py">selectedDistance</span><span class="p">:</span> <span class="nc">ObjectProperty</span><span class="p">&lt;</span><span class="nc">Distance</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">SimpleObjectProperty</span><span class="p">()</span>
    <span class="kd">val</span> <span class="py">selectedManifold</span><span class="p">:</span> <span class="nc">ObjectProperty</span><span class="p">&lt;</span><span class="nc">Manifold</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">SimpleObjectProperty</span><span class="p">()</span>
    <span class="kd">val</span> <span class="py">toleranceManual</span><span class="p">:</span> <span class="nc">DoubleProperty</span> <span class="p">=</span> <span class="nc">SimpleDoubleProperty</span><span class="p">()</span>
    <span class="kd">val</span> <span class="py">useAll</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="nc">SimpleBooleanProperty</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">useVisible</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="nc">SimpleBooleanProperty</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">manifoldCullFace</span><span class="p">:</span> <span class="nc">ObjectProperty</span><span class="p">&lt;</span><span class="nc">CullFace</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">SimpleObjectProperty</span><span class="p">()</span>
    <span class="kd">val</span> <span class="py">showWireFrame</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="nc">SimpleBooleanProperty</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">showControlPoints</span><span class="p">:</span> <span class="nc">BooleanProperty</span> <span class="p">=</span> <span class="nc">SimpleBooleanProperty</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">selectedFactorLabel</span><span class="p">:</span> <span class="nc">StringProperty</span> <span class="p">=</span> <span class="nc">SimpleStringProperty</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>
<p>In truth, I got fed up searching through all of the <code class="language-plaintext highlighter-rouge">ManifoldEvents</code> to find out what data was being passed back and forth to other parts of the application.  So I’m sure that this <code class="language-plaintext highlighter-rouge">SharedElements</code> object is missing quite a few elements.  There’s enough here to make the point, though, and without the rest of the application it doesn’t make any difference for this demonstration.</p>

<h2 id="the-interactor">The Interactor</h2>

<p>The last MVCI component is the Interactor:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">FromFxmlInteractor</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">model</span><span class="p">:</span> <span class="nc">FromFxmlModel</span><span class="p">,</span> <span class="k">private</span> <span class="kd">val</span> <span class="py">externalFunctions</span><span class="p">:</span> <span class="nc">SharedFunctions</span><span class="p">)</span> <span class="p">{</span>

    <span class="k">private</span> <span class="kd">var</span> <span class="py">umapDto</span><span class="p">:</span> <span class="nc">UmapDto</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span>

    <span class="nf">init</span> <span class="p">{</span>
        <span class="nf">createDummyData</span><span class="p">()</span>
        <span class="n">model</span><span class="p">.</span><span class="n">selectedDistance</span><span class="p">.</span><span class="nf">subscribe</span> <span class="p">{</span> <span class="n">oldValue</span><span class="p">,</span> <span class="n">newValue</span> <span class="p">-&gt;</span>
            <span class="n">oldValue</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span>
                <span class="n">model</span><span class="p">.</span><span class="n">connectorThickness</span><span class="p">.</span><span class="nf">unbindBidirectional</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">width</span><span class="p">)</span>
                <span class="n">model</span><span class="p">.</span><span class="n">connectorColour</span><span class="p">.</span><span class="nf">unbindBidirectional</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">colour</span><span class="p">)</span>
            <span class="p">}</span>
            <span class="n">newValue</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span>
                <span class="n">model</span><span class="p">.</span><span class="n">connectorThickness</span><span class="p">.</span><span class="nf">bindBidirectional</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">width</span><span class="p">)</span>
                <span class="n">model</span><span class="p">.</span><span class="n">connectorColour</span><span class="p">.</span><span class="nf">bindBidirectional</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">colour</span><span class="p">)</span>
            <span class="p">}</span>
            <span class="n">model</span><span class="p">.</span><span class="n">externallySelectedDistance</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="n">newValue</span>
        <span class="p">}</span>
        <span class="n">model</span><span class="p">.</span><span class="n">selectedManifold</span><span class="p">.</span><span class="nf">subscribe</span> <span class="p">{</span> <span class="n">oldValue</span><span class="p">,</span> <span class="n">newValue</span> <span class="p">-&gt;</span>
            <span class="n">oldValue</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span>
                <span class="n">model</span><span class="p">.</span><span class="n">manifoldDiffuseColour</span><span class="p">.</span><span class="nf">unbindBidirectional</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">diffuseColour</span><span class="p">)</span>
                <span class="n">model</span><span class="p">.</span><span class="n">manifoldSpecularColour</span><span class="p">.</span><span class="nf">unbindBidirectional</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">specularColour</span><span class="p">)</span>
                <span class="n">model</span><span class="p">.</span><span class="n">manifoldWireMeshColour</span><span class="p">.</span><span class="nf">unbindBidirectional</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">wireframeColour</span><span class="p">)</span>
            <span class="p">}</span>
            <span class="n">newValue</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span>
                <span class="n">model</span><span class="p">.</span><span class="n">manifoldDiffuseColour</span><span class="p">.</span><span class="nf">bindBidirectional</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">diffuseColour</span><span class="p">)</span>
                <span class="n">model</span><span class="p">.</span><span class="n">manifoldSpecularColour</span><span class="p">.</span><span class="nf">bindBidirectional</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">specularColour</span><span class="p">)</span>
                <span class="n">model</span><span class="p">.</span><span class="n">manifoldWireMeshColour</span><span class="p">.</span><span class="nf">bindBidirectional</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">wireframeColour</span><span class="p">)</span>
            <span class="p">}</span>
            <span class="n">model</span><span class="p">.</span><span class="n">externallySelectedManifold</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="n">newValue</span>
        <span class="p">}</span>

    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">createDummyData</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">model</span><span class="p">.</span><span class="n">distanceList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">Distance</span><span class="p">(</span><span class="s">"Label 1"</span><span class="p">,</span> <span class="s">"Millimetres"</span><span class="p">,</span> <span class="mf">17.0</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="nc">Color</span><span class="p">.</span><span class="nc">GREEN</span><span class="p">))</span>
        <span class="n">model</span><span class="p">.</span><span class="n">distanceList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">Distance</span><span class="p">(</span><span class="s">"Label 2"</span><span class="p">,</span> <span class="s">"Nanometres"</span><span class="p">,</span> <span class="mf">22.0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="nc">Color</span><span class="p">.</span><span class="nc">CYAN</span><span class="p">))</span>
        <span class="n">model</span><span class="p">.</span><span class="n">distanceList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">Distance</span><span class="p">(</span><span class="s">"Label 3"</span><span class="p">,</span> <span class="s">"Millimetres"</span><span class="p">,</span> <span class="mf">8.0</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="nc">Color</span><span class="p">.</span><span class="nc">AZURE</span><span class="p">))</span>
        <span class="n">model</span><span class="p">.</span><span class="n">distanceList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">Distance</span><span class="p">(</span><span class="s">"Label 4"</span><span class="p">,</span> <span class="s">"Millimetres"</span><span class="p">,</span> <span class="mf">45.0</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="nc">Color</span><span class="p">.</span><span class="nc">RED</span><span class="p">))</span>
        <span class="n">model</span><span class="p">.</span><span class="n">manifoldList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">Manifold</span><span class="p">(</span><span class="s">"Label 1"</span><span class="p">))</span>
        <span class="n">model</span><span class="p">.</span><span class="n">manifoldList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">Manifold</span><span class="p">(</span><span class="s">"Label 2"</span><span class="p">))</span>
        <span class="n">model</span><span class="p">.</span><span class="n">manifoldList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">Manifold</span><span class="p">(</span><span class="s">"Label 3"</span><span class="p">))</span>
        <span class="n">model</span><span class="p">.</span><span class="n">manifoldList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">Manifold</span><span class="p">(</span><span class="s">"Label 4"</span><span class="p">))</span>
    <span class="p">}</span>

    <span class="k">fun</span> <span class="nf">buildCluster</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">externalFunctions</span><span class="p">.</span><span class="n">buildCluster</span><span class="p">.</span><span class="nf">invoke</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">fun</span> <span class="nf">generate</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">externalFunctions</span><span class="p">.</span><span class="n">generate</span><span class="p">.</span><span class="nf">invoke</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">fun</span> <span class="nf">clearAll</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">externalFunctions</span><span class="p">.</span><span class="n">clearAll</span><span class="p">.</span><span class="nf">invoke</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">fun</span> <span class="nf">exportAll</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">externalFunctions</span><span class="p">.</span><span class="n">exportAll</span><span class="p">.</span><span class="nf">invoke</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">fun</span> <span class="nf">clearDistances</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">externalFunctions</span><span class="p">.</span><span class="n">clearDistances</span><span class="p">.</span><span class="nf">invoke</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">fun</span> <span class="nf">project</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">externalFunctions</span><span class="p">.</span><span class="n">project</span><span class="p">.</span><span class="nf">invoke</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">fun</span> <span class="nf">exportMatrix</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">externalFunctions</span><span class="p">.</span><span class="n">exportMatrix</span><span class="p">.</span><span class="nf">invoke</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">fun</span> <span class="nf">saveProjections</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">externalFunctions</span><span class="p">.</span><span class="n">saveProjections</span><span class="p">.</span><span class="nf">invoke</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">fun</span> <span class="nf">runPca</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">externalFunctions</span><span class="p">.</span><span class="n">runPCA</span><span class="p">.</span><span class="nf">invoke</span><span class="p">()</span>
    <span class="p">}</span>


    <span class="k">fun</span> <span class="nf">saveUmap</span><span class="p">(</span><span class="n">file</span><span class="p">:</span> <span class="nc">File</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">model</span><span class="p">.</span><span class="n">latestDir</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="n">file</span>
    <span class="p">}</span>


    <span class="k">fun</span> <span class="nf">completeSaveUmap</span><span class="p">()</span> <span class="p">{}</span>

    <span class="k">fun</span> <span class="nf">loadUmap</span><span class="p">(</span><span class="n">file</span><span class="p">:</span> <span class="nc">File</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"Hey!  Loading a file"</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">fun</span> <span class="nf">completeLoadUmap</span><span class="p">()</span> <span class="p">{}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">init{}</code> section is much like a constructor, and here it creates all of the relationships between elements of the Model that would be considered “business/application logic”.  In this case it is mostly dealing with the relationships between the actively selected Manifold or Distance and some of the other properties.</p>

<p>The rest of this feels much more like a skeleton than it really is.  The file handling methods are just placeholders, as they would need to connect to a service of some sort which would do the heavy lifting.</p>

<p>All of the other methods, however, are pretty much in their final form.  They just need to invoke the functional elements provide by <code class="language-plaintext highlighter-rouge">ExternalFunctions</code>, which looks like this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">SharedFunctions</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">buildCluster</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span> <span class="p">=</span> <span class="p">{}</span>
    <span class="kd">val</span> <span class="py">generate</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span> <span class="p">=</span> <span class="p">{}</span>
    <span class="kd">val</span> <span class="py">clearAll</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span> <span class="p">=</span> <span class="p">{}</span>
    <span class="kd">val</span> <span class="py">exportAll</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span> <span class="p">=</span> <span class="p">{}</span>
    <span class="kd">val</span> <span class="py">clearDistances</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span> <span class="p">=</span> <span class="p">{}</span>
    <span class="kd">val</span> <span class="py">project</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span> <span class="p">=</span> <span class="p">{}</span>
    <span class="kd">val</span> <span class="py">exportMatrix</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span> <span class="p">=</span> <span class="p">{}</span>
    <span class="kd">val</span> <span class="py">saveProjections</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span> <span class="p">=</span> <span class="p">{}</span>
    <span class="kd">val</span> <span class="py">runPCA</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span> <span class="p">=</span> <span class="p">{}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>These would ordinarily be defined in some other part of the application which is actually going to do the work.  These functions completely replace all of the <code class="language-plaintext highlighter-rouge">ManifoldEvent</code> firings in the original FXML Controller.</p>

<h1 id="conclusion">Conclusion</h1>

<p>I’m not going to pretend that I understand what this project does, but my impression is that it involves really complicated and sophisticated analysis of some kind of AI processing.  But when you look at this screen it’s really just a bunch of <code class="language-plaintext highlighter-rouge">Controls</code> and <code class="language-plaintext highlighter-rouge">Buttons</code> and <code class="language-plaintext highlighter-rouge">Lists</code> that manipulate some data and trigger some actions.</p>

<p>The original design leaks the complexity of the entire application into what should be a simple screen.  You cannot change a data value without knowing how that will impact the rest of the application.</p>

<p>I need to stress that the code that I’ve published here runs as a stand-alone application.  It doesn’t connect to anything, but it works and can be integrated into the rest of the application simply by providing the shared data and functions in the Controller constructor.</p>

<p>Clearly, a lot more was done here than just replace the FXML with code, although it’s fairly clear that the layout code is much simpler than the FXML plus FXML Controller from the original.</p>

<h2 id="the-layout">The Layout</h2>

<p>I deliberately put this project aside for a while so that I could come back to get a more objective sense of how easy it is to read and understand the 230 lines of layout code.</p>

<p>One thing that was immediately clear to me when I came back to it was that none of these <code class="language-plaintext highlighter-rouge">Tabs</code> have anything to do with each other except that they cohabit in the same <code class="language-plaintext highlighter-rouge">TabPane</code>.  As such, they could all be defined in their own builders, and each one would, therefore, be a little bit easier to understand since they wouldn’t be encumbered with the code from the other <code class="language-plaintext highlighter-rouge">Tabs</code>.</p>

<p>Furthermore, each of these <code class="language-plaintext highlighter-rouge">Tabs</code> could have their own, independent, MVCI structure associated with them.  There could be a “master” MVCI structure associated with the <code class="language-plaintext highlighter-rouge">TabPane</code> itself, and its Controller could handle instantiation of all of the other MVCI structures.  This would make each of the 4 separate MVCI constructs extremely simple and easy to understand.</p>

<div class="notice--primary">
 <img src="/assets/logos/brain.png" alt="primary" style="float:left;margin-right: 10px" />
 <p style="overflow:auto; float:none">
   Even if a Button on one Tab required the external function it invoked to use data from another Tab this wouldn't matter because the Button actions aren't transferring any data - that's already handled by the shared data elements.
 </p>
</div>

<h2 id="using-a-framework">Using a Framework</h2>

<p>I simply cannot imagine building anything like this without implementing a framework of some kind.</p>

<p>One of the things that became glaringly apparent after the conversion was that this screen, aside from some file handling, doesn’t actually <em>do</em> anything itself.  You can see this just from looking at the Interactor.  It doesn’t have much code that actually does anything.  It just dispatches actions off to some other part of the application.</p>

<p>Certainly, if you had written the application, or if you were very familiar with the entire application, you’d <em>know</em> that this screen didn’t actually do anything.  But this is absolutely <strong>not</strong> clear from a casual glance at the original code.</p>

<h2 id="reactive-vs-imperative-design">Reactive vs Imperative Design</h2>

<p>A much as I think the coded layout is a win compared to FXML, I think that this exercise really illustrates the wonderful simplicity that comes from implementing a Reactive design.  There are literally hundreds of lines of code in the FXML Controller that just vanish away when a Reactive design is implemented.</p>

<p>Using a Reactive design also makes it trivial to connect to external elements of the application through a shared data model.  This approach also greatly simplifies the understanding of the coupling between this screen and those external elements as it is <em>all</em> defined inside that single object.</p>

<h2 id="coupling">Coupling</h2>

<p>Coupling in this new design is extremely controlled, and easy to understand.</p>

<p>The Model is the main source of coupling, but it also isolates as well.  There’s no way for anything outside of the View to know if a <code class="language-plaintext highlighter-rouge">Boolean</code> value in the Model is presented to the user via a <code class="language-plaintext highlighter-rouge">RadioButton</code>, a <code class="language-plaintext highlighter-rouge">ToggleButton</code>, a <code class="language-plaintext highlighter-rouge">CheckBox</code> or some custom <code class="language-plaintext highlighter-rouge">Control</code>.  But, no matter how it’s handled in the View, the Interactor can always simply deal with the <code class="language-plaintext highlighter-rouge">Boolean</code> value that it is bound to.</p>

<p>In a similar manner, the <code class="language-plaintext highlighter-rouge">SharedElements</code> is the main source of both coupling and isolation between this screen and the rest of the application.</p>

<div class="notice--primary">
 <img src="/assets/logos/brain.png" alt="primary" style="float:left;margin-right: 10px" />
 <p style="overflow:auto; float:none">
   Coupling is usually the single biggest source of unecessary complexity in any application.  Controlling coupling is the best way to improve code quality.
 </p>
</div>

<p>I simply cannot stress this too much.  Virtually every good (or “clean”) coding technique, is designed to control and eliminate coupling as much possible.  Looking at this “Trinity” project, excessive coupling is everywhere and it makes everything much more complicated than it needs to be.  I’ve tried to limit coupling as much as possible in my version, and I think it is reflected in the lack of complexity.</p>

<h2 id="the-kotlin">The Kotlin</h2>

<p>It’s really clear from this example just how much Kotlin lets you extract the boilerplate and verbosity out of the layout code.  The infix extension decorator functions mean that you never have to instantiate any element of the layout as a variable.  In most cases the instantiation, configuration, binding and addition to the layout of the <code class="language-plaintext highlighter-rouge">Nodes</code> is done in a single line.</p>

<p>Maybe (probably?) you don’t want to learn Kotlin to do this.  In Java, I think you’d have to create classic builders with the configuration elements included as decorators.  But you could do a lot of this stuff that way.</p>

<p>What I will say is that if you are looking for a tool to make layout creation and maintenance easier, you’ll get a lot more mileage out of learning Kotlin than you will by mastering SceneBuilder and FXML.</p>]]></content><author><name>Dave Barrett</name><email>pragmaticcoding8@gmail.com</email></author><category term="javafx" /><summary type="html"><![CDATA[I've talked a lot about how coded layouts are better than FXML. In this article we take a fairly large FXML based layout which is part of a larger project, convert it to code, and implement a framework using a Reactive design. Is the final result better than FXML? See for yourself.]]></summary></entry><entry><title type="html">Reactive Programming in JavaFX</title><link href="https://www.pragmaticcoding.ca/javafx/elements/reactive-javafx" rel="alternate" type="text/html" title="Reactive Programming in JavaFX" /><published>2025-07-16T17:00:00+00:00</published><updated>2025-07-16T17:00:00+00:00</updated><id>https://www.pragmaticcoding.ca/javafx/elements/reactive-javafx</id><content type="html" xml:base="https://www.pragmaticcoding.ca/javafx/elements/reactive-javafx"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>From time to time the question comes up about Swing vs JavaFX.  Which is better?  Is Swing dead?  Should you convert your old Swing application to JavaFX?</p>

<p>People always tend to focus in these discussions on the look and feel of Swing.  Out-of-the-box GUI’s built in Swing tend to look like refuges from the 1990’s, which isn’t surprising considering the age of Swing.  JavaFX tends to look more modern by default, although still very “corporate”.  But it seems easier to customize the look and feel with JavaFX.</p>

<p>But I’m not sure that any of that is enough reason to go through a painful conversion from Swing to JavaFX.</p>

<div class="notice--primary">
 <img src="/assets/logos/brain.png" alt="primary" style="float:left;margin-right: 10px" />
 <p style="overflow:auto; float:none">
   The most important difference between JavaFX and Swing is that JavaFX is designed to support Reactive GUI development.<br />
 </p>
</div>

<p>When you look at the scope of Observable classes and utilities this - at least to me - becomes obvious.</p>

<p>Yes, Swing <em>does</em> support the “Observer Pattern”, but it is extremely crude compared to JavaFX. The vast library of JavaFX Properties, Bindings, Listeners and Subscriptions, and the way that they are integrated natively into all of the screen <code class="language-plaintext highlighter-rouge">Nodes</code>, make it virtually trivial to implement a Reactive framework.</p>

<div class="notice--primary">
 <img src="/assets/logos/brain.png" alt="primary" style="float:left;margin-right: 10px" />
 <p style="overflow:auto; float:none">
   My experience has been that applications architected with a Reactive design are perhaps an order of magnitude easier to implement, enhance and maintain.  Now that could be a valid reason to convert a Swing application to JavaFX.<br />
 </p>
</div>

<p>The problem, though, is that it is also possible to write a JavaFX application in an imperative/declarative fashion.  And it works, although it’s not as clean as using a Reactive approach.</p>

<p>Even worse, nobody talks about “Reactive JavaFX”.  It’s not mentioned at all in the Oracle documentation or tutorials.  It’s a shame.</p>

<p>In this article we are going to look at how Reactive GUI’s work, the different kinds of Reactive systems that are available and how JavaFX fits in with those.  Then we are going to look at how a Reactive JavaFX system is different from a Declarative/Imperative system, and we’ll see how Reactive design makes the application simpler by removing coupling.  Finally, we’ll look at how Reactive Design fits in with frameworks like MVC, MVVM and MVCI.</p>

<h1 id="what-is-reactive-design">What is Reactive Design?</h1>

<p>Any “Reactive” design involves creating a data representation of the “State” of your GUI.  When the values in this State construct change, then the Reactive system will update the GUI such that it reflects those changes.</p>

<p>The layout programmer’s job, therefore, is define how the layout will look and behave with any given set of values in the State.</p>

<p>When you start to work with a Reactive system, you realize that there are two kinds of things that can happen in a GUI:</p>

<ol>
  <li>
    <p>Changes to State<br />For instance a user types something in a textfield, moves a slider, or selects an item in a list.</p>
  </li>
  <li>
    <p>Actions<br />This could be a user clicking on a button, or actions associated with the user moving a slider or selecting an item in a list.</p>
  </li>
</ol>

<p>In a Declarative (or “Imperative”) approach to GUI design, everything is an “action”.  For instance, the user types in a textfield, but this affects only the screen until they shift the focus to another field, at which point a “focus lost” action might be triggered that would cause code to run that moves data around or changes the layout.</p>

<p>A Reactive system usually looks something like this:</p>

<p><img src="/assets/elements/Reactive.png" alt="Diagram" /></p>

<p>The layout has elements that control how it behaves that are “bound” or “synchronized” in both directions with the “Presentation Model”, which is the data representation of “State”.</p>

<p>Additionally, actions might be triggered by the layout.  Whatever handles these actions also has access to the data elements in the Presentation Model, but usually in a more normal read and write manner - not through binding.  It is possible that the “Action Handler” could tell the layout to take some action, although I’m not sure that all Reactive systems allow this.</p>

<h1 id="types-of-reactive-systems">Types of Reactive Systems</h1>

<p>Personally, I’ve encountered three different Reactive systems: JavaFX, Jetpack Compose, and React.</p>

<p>Jetpack Compose is the preferred system for devoloping Android applications, while React is for web development and JavaFX, of course, is for desktop applications.  I have some experience with Jetpack Compose, and a passing familiarity with React.</p>

<p>From what I have seen, these break down into two different approaches to Reactivity.  I’ve never seen them defined anywhere, so I am going to call them “Compositional Reactivity” and “Layout Reactivity”.  Let’s take a look at them…</p>

<h2 id="compostional-reactivity">Compostional Reactivity</h2>

<p>This is the implementation of Reactivity that is use by both Jetpack Compose and React.</p>

<p>Essentially, the code that “composes” the layout is divided up into snippets, and each snippet is dependent on whatever elements of State that it uses.  If one of those State elements changes, then that code snippet (and presumably any code snippets that it calls) will be re-executed and redraw the screen.  All of the screen elements themselves are truly static, and depend on the re-execution of the composition code in order to appear to behave dynamically.</p>

<p>With Compositional Reactivity, you expect that your layout code is going to be run over and over again - at least in part.  Each time it executes, it will build the static layout according the values in the Presentation Model at the time that it executes.</p>

<p>Here’s an example from the official Jetpack Compose tutorial:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Composable</span>
<span class="k">fun</span> <span class="nf">WaterCounter</span><span class="p">(</span><span class="n">modifier</span><span class="p">:</span> <span class="nc">Modifier</span> <span class="p">=</span> <span class="nc">Modifier</span><span class="p">)</span> <span class="p">{</span>
   <span class="nc">Column</span><span class="p">(</span><span class="n">modifier</span> <span class="p">=</span> <span class="n">modifier</span><span class="p">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">16</span><span class="p">.</span><span class="n">dp</span><span class="p">))</span> <span class="p">{</span>
       <span class="c1">// Changes to count are now tracked by Compose</span>
       <span class="kd">val</span> <span class="py">count</span><span class="p">:</span> <span class="nc">MutableState</span><span class="p">&lt;</span><span class="nc">Int</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nf">mutableStateOf</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>

       <span class="nc">Text</span><span class="p">(</span><span class="s">"You've had ${count.value} glasses."</span><span class="p">)</span>
        <span class="nc">Button</span><span class="p">(</span><span class="n">onClick</span> <span class="p">=</span> <span class="p">{</span> <span class="n">count</span><span class="p">.</span><span class="n">value</span><span class="p">++</span> <span class="p">},</span> <span class="nc">Modifier</span><span class="p">.</span><span class="nf">padding</span><span class="p">(</span><span class="n">top</span> <span class="p">=</span> <span class="mi">8</span><span class="p">.</span><span class="n">dp</span><span class="p">))</span> <span class="p">{</span>
           <span class="nc">Text</span><span class="p">(</span><span class="s">"Add one"</span><span class="p">)</span>
       <span class="p">}</span>
   <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">@Composable</code> annotation declares this function as something that Jetpack Compose is going to use to build the layout and which is dependent on State.  This function creates a <code class="language-plaintext highlighter-rouge">Column</code> and sticks a <code class="language-plaintext highlighter-rouge">Text</code> and a <code class="language-plaintext highlighter-rouge">Button</code> in it.</p>

<p>The line that instantiates <code class="language-plaintext highlighter-rouge">count</code> as <code class="language-plaintext highlighter-rouge">MutableState&lt;Int&gt;</code> is where it defines the element of State that the this function is dependent on.  Changes to <code class="language-plaintext highlighter-rouge">count</code> will trigger re-execution of this code - but let’s just call it “recomposition”.</p>

<p>Notice that the <code class="language-plaintext highlighter-rouge">Text</code> just shows a static String that includes the current value of <code class="language-plaintext highlighter-rouge">count</code> at the time that it executes.  There is no way to change the value displayed by a <code class="language-plaintext highlighter-rouge">Text</code>.</p>

<p>The <code class="language-plaintext highlighter-rouge">Button</code> just increments <code class="language-plaintext highlighter-rouge">count</code>.  Here you see how an action updates the Presentation Model, and that update triggers the recomposition of the layout.</p>

<p>As soon as <code class="language-plaintext highlighter-rouge">count</code> changes, this piece of layout is determined to be defunct and needs to be re-composed, so this function will be re-executed, the <code class="language-plaintext highlighter-rouge">Column</code> will be rebuilt and the <code class="language-plaintext highlighter-rouge">Text</code> will be recreated and populated with a String that now contains the new value of <code class="language-plaintext highlighter-rouge">count</code>.</p>

<p>To the user, it just looks like the value in the <code class="language-plaintext highlighter-rouge">Text</code> changed.  But from a programming perspective, a piece of the layout has been recomposed.</p>

<p>You can put any logic that you want in there.  Try this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Composable</span>
<span class="k">fun</span> <span class="nf">WaterCounter</span><span class="p">(</span><span class="n">modifier</span><span class="p">:</span> <span class="nc">Modifier</span> <span class="p">=</span> <span class="nc">Modifier</span><span class="p">)</span> <span class="p">{</span>
   <span class="nc">Column</span><span class="p">(</span><span class="n">modifier</span> <span class="p">=</span> <span class="n">modifier</span><span class="p">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">16</span><span class="p">.</span><span class="n">dp</span><span class="p">))</span> <span class="p">{</span>
       <span class="c1">// Changes to count are now tracked by Compose</span>
       <span class="kd">val</span> <span class="py">count</span><span class="p">:</span> <span class="nc">MutableState</span><span class="p">&lt;</span><span class="nc">Int</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nf">mutableStateOf</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>

       <span class="k">if</span> <span class="p">(</span><span class="n">count</span> <span class="p">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
          <span class="nc">Text</span><span class="p">(</span><span class="s">"Glug...glug...glug..."</span><span class="p">)</span>
       <span class="p">}</span>
       <span class="nc">Text</span><span class="p">(</span><span class="s">"You've had ${count.value} glasses."</span><span class="p">)</span>
       <span class="nc">Button</span><span class="p">(</span><span class="n">onClick</span> <span class="p">=</span> <span class="p">{</span> <span class="n">count</span><span class="p">.</span><span class="n">value</span><span class="p">++</span> <span class="p">},</span> <span class="nc">Modifier</span><span class="p">.</span><span class="nf">padding</span><span class="p">(</span><span class="n">top</span> <span class="p">=</span> <span class="mi">8</span><span class="p">.</span><span class="n">dp</span><span class="p">))</span> <span class="p">{</span>
           <span class="nc">Text</span><span class="p">(</span><span class="s">"Add one"</span><span class="p">)</span>
       <span class="p">}</span>
   <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The first time through <code class="language-plaintext highlighter-rouge">count</code> will be “0” and that “Glug..glug…glug…” <code class="language-plaintext highlighter-rouge">Text</code> will not appear.  Every other time, count will be non-zero and that <code class="language-plaintext highlighter-rouge">Text</code> will appear.</p>

<h2 id="layout-reactivity">Layout Reactivity</h2>

<p>With “Layout Reactivity” the layout code is executed just once, but the layout itself behaves dynamically in response to changes to the Presentation Model.  This is the approach that JavaFX uses.</p>

<p>Let’s see how you would do the same type of counting exercise as in the Jetpack Compose example:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">waterCounter</span><span class="p">()</span> <span class="p">:</span> <span class="nc">Region</span> <span class="p">=</span> <span class="nc">VBox</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">counter</span> <span class="p">:</span><span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
    <span class="n">children</span> <span class="p">+=</span> <span class="nc">Label</span><span class="p">(</span><span class="s">"Glug...glug...glug..."</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
       <span class="nf">visibleProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="n">counter</span><span class="p">.</span><span class="nf">greaterThan</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span>
       <span class="nf">managedProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="nf">visibleProperty</span><span class="p">())</span>
    <span class="p">}</span>
    <span class="n">children</span> <span class="p">+=</span> <span class="nc">Label</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
       <span class="nf">textProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="n">counter</span><span class="p">.</span><span class="nf">map</span><span class="p">{</span> <span class="s">"You've had $it glasses."</span> <span class="p">})</span>
    <span class="p">}</span>
    <span class="n">childern</span> <span class="p">+=</span> <span class="nc">Button</span><span class="p">(</span><span class="s">"Add one"</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
       <span class="nf">setOnClick</span> <span class="p">{</span> <span class="n">evt</span> <span class="p">-&gt;</span> <span class="n">counter</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="mi">1</span> <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Here we have a static layout that behaves dynamically:  It’s “static” in that layout itself doesn’t change.  No <code class="language-plaintext highlighter-rouge">Nodes</code> are added or removed from the layout.  It behaves dynamically, because the individual <code class="language-plaintext highlighter-rouge">Nodes</code> do things in reaction to the Presentation Model.</p>

<p>That “Glug…” <code class="language-plaintext highlighter-rouge">Text</code> is always there.  However, its visibility is bound to whether or not <code class="language-plaintext highlighter-rouge">count</code> is greater than zero.  Its <code class="language-plaintext highlighter-rouge">managed Property</code> which controls whether or not the layout manager gives it space on the screen is also bound to the same condition.  When <code class="language-plaintext highlighter-rouge">count</code> is zero, it’s neither visible, nor given any space on the screen.</p>

<p>The text in the second <code class="language-plaintext highlighter-rouge">Label</code> is bound to <code class="language-plaintext highlighter-rouge">counter</code> through a <code class="language-plaintext highlighter-rouge">map{}</code> functiont that provides the rest of the String.</p>

<p>Once again, the <code class="language-plaintext highlighter-rouge">Button</code> simply triggers an action that updates the Presentation Model which, in turn, causes the layout to react and change its behaviour.</p>

<h2 id="these-are-very-different-approaches">These are Very Different Approaches</h2>

<p>I think you can see that to a user, there is no difference between either approach.  The GUI’s appear to behave in an identical manner.</p>

<p>It’s the implementation which is <strong>completely</strong> different.</p>

<p>In the Jetpack Compose approach, that “Glug…” <code class="language-plaintext highlighter-rouge">Text</code> didn’t even exist in the layout before the <code class="language-plaintext highlighter-rouge">Button</code> was clicked.  In the JavaFX version, it was always there - just invisible.</p>

<p>The truth is that both approaches boil down to the same thing.  At some point the screen has to be redrawn to show that “Glug…” <code class="language-plaintext highlighter-rouge">Label\Text</code>.  But with JavaFX, that redrawing happens deep, deep inside the guts of the Layout Manager, and as programmers we don’t ever need to think about it.  In Jetpack Compose and React, that’s just about <em>all</em> that you think about - “How will you build the layout when the data looks like this..or this..or this?”</p>

<p>Obviously, Jetpack Compose and React are far more popular than JavaFX, so when people think about “Reactive” systems, they are probably imagining a Compositionally Reactive design.</p>

<h1 id="how-reactive-design-makes-systems-better">How Reactive Design Makes Systems Better</h1>

<p>Back in the introduction, I claimed that Reactive Design makes systems nearly an order of magnitude easier to enhance, debug and maintain.  Let’s look at how that might be…</p>

<h2 id="the-same-example-in-an-imperative-design">The Same Example In an Imperative Design</h2>

<p>Before we go any further, let’s look at the example, and how you would write it in JavaFX like it was Swing:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">waterCounter</span><span class="p">()</span> <span class="p">:</span> <span class="nc">Region</span> <span class="p">=</span> <span class="nc">VBox</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">counter</span> <span class="p">:</span> <span class="nc">Int</span> <span class="p">=</span> <span class="mi">0</span>
    <span class="kd">val</span> <span class="py">glugLabel</span> <span class="p">=</span> <span class="nc">Label</span><span class="p">(</span><span class="s">"Glug...glug...glug..."</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">countLabel</span> <span class="p">=</span> <span class="nc">Label</span><span class="p">(</span><span class="s">"You've had $counter glasses."</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">button</span> <span class="p">=</span> <span class="nc">Button</span><span class="p">(</span><span class="s">"Add one"</span><span class="p">)</span>
    <span class="n">button</span><span class="p">.</span><span class="nf">setOnAction</span> <span class="p">{</span> <span class="n">evt</span> <span class="p">-&gt;</span>
       <span class="n">countLabel</span><span class="p">.</span><span class="nf">setText</span><span class="p">(</span><span class="s">"You've had $counter glasses."</span><span class="p">)</span>
       <span class="nf">getChildren</span><span class="p">().</span><span class="nf">setAll</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="n">glugLabel</span><span class="p">,</span> <span class="n">countLabel</span><span class="p">,</span> <span class="n">button</span><span class="p">))</span>
    <span class="p">}</span>
    <span class="n">children</span> <span class="p">+=</span> <span class="nf">listOf</span><span class="p">(</span><span class="n">countLabel</span><span class="p">,</span> <span class="n">button</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Instead of a <code class="language-plaintext highlighter-rouge">Binding</code>, the value of <code class="language-plaintext highlighter-rouge">countLabel</code> is changed via <code class="language-plaintext highlighter-rouge">setText()</code>, and instead of using a static layout with <code class="language-plaintext highlighter-rouge">glugLabel</code> toggling between invisible and visible, the layout is modified to add <code class="language-plaintext highlighter-rouge">glugLabel</code> in response to the <code class="language-plaintext highlighter-rouge">Button</code> click.</p>

<h2 id="coupling">Coupling</h2>

<p>One thing you’ll notice in this Imperative example is that we now have variables to hold <code class="language-plaintext highlighter-rouge">Labels</code> and <code class="language-plaintext highlighter-rouge">Button</code>.  That’s because we need references to them so that we can update them from inside the <code class="language-plaintext highlighter-rouge">Button</code> action.  You’ll also see that <code class="language-plaintext highlighter-rouge">button</code> is calling a method on the enclosing VBox - <code class="language-plaintext highlighter-rouge">getChildren()</code>.</p>

<p>This are examples of “coupling”.</p>

<div class="notice--primary">
 <img src="/assets/logos/brain.png" alt="primary" style="float:left;margin-right: 10px" />
 <p style="overflow:auto; float:none">
   Coupling is probably the single most important factor that makes applications difficult to understand, maintain and enhance.<br />
 </p>
</div>

<p>When we expose <code class="language-plaintext highlighter-rouge">glugLabel</code>, <code class="language-plaintext highlighter-rouge">countLabel</code> and the enclosing <code class="language-plaintext highlighter-rouge">VBox</code> to <code class="language-plaintext highlighter-rouge">button</code>, we don’t just reveal their existence, but also their implementation.</p>

<p>Once we’ve shared the implementation details of something, then we’ve created coupling.  It’s coupling because we cannot change that implementation without looking at how that implementation knowledge is used through the system.</p>

<p>Even here, where the variable scope is just 10 lines of code, it can cause problems.  Imagine that we wanted to change the text of <code class="language-plaintext highlighter-rouge">counterLabel</code> to this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">countLabel</span> <span class="p">=</span> <span class="nc">Label</span><span class="p">(</span><span class="s">"You've had $counter pints of beer."</span><span class="p">)</span>
</code></pre></div></div>
<p>We’ll have a problem if we just stop there, because <code class="language-plaintext highlighter-rouge">button</code> is going to set it back to <code class="language-plaintext highlighter-rouge">"You've had $counter glasses."</code> as soon as it is clicked.  Any change to the implementation of <code class="language-plaintext highlighter-rouge">countLabel</code> means our application might be broken.</p>

<p>Let’s say that we decided to change our implementation of <code class="language-plaintext highlighter-rouge">counterLabel</code> to be something other than a <code class="language-plaintext highlighter-rouge">Label</code>.  Perhaps an <code class="language-plaintext highlighter-rouge">ImageView</code> sprite with a number of <code class="language-plaintext highlighter-rouge">Images</code> of different numbers of beer glasses, tied somehow to <code class="language-plaintext highlighter-rouge">counter</code>.  Now, our <code class="language-plaintext highlighter-rouge">Button</code> action won’t work at all, and we’ll have to change its logic to manipulate an <code class="language-plaintext highlighter-rouge">ImageView</code> instead.  At least here we’ll get a compiler error.</p>

<p>This situation gets worse if <code class="language-plaintext highlighter-rouge">button</code> is defined somewhere else.  Then, we’ll need to push <code class="language-plaintext highlighter-rouge">countLabel</code> and our <code class="language-plaintext highlighter-rouge">VBox</code> up to fields in our layout class, so that we can access them with <code class="language-plaintext highlighter-rouge">button</code>:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   <span class="k">private</span> <span class="kd">val</span> <span class="py">countLabel</span> <span class="p">=</span> <span class="nc">Label</span><span class="p">(</span><span class="s">"You've had 0 glasses."</span><span class="p">)</span>
   <span class="k">private</span> <span class="kd">val</span> <span class="py">countPane</span> <span class="p">:</span> <span class="nc">Pane</span> <span class="p">=</span> <span class="nf">waterCounter</span><span class="p">()</span>
     <span class="p">.</span>
     <span class="p">.</span>
     <span class="p">.</span>
   <span class="k">fun</span> <span class="nf">waterCounter</span><span class="p">()</span> <span class="p">:</span> <span class="nc">Pane</span> <span class="p">=</span> <span class="nc">VBox</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
         <span class="p">.</span>
         <span class="p">.</span>
      <span class="n">children</span> <span class="p">+=</span> <span class="n">countLabel</span>   
         <span class="p">.</span>
         <span class="p">.</span>
   <span class="p">}</span>   
      <span class="p">.</span>
      <span class="p">.</span>
      <span class="p">.</span>
   <span class="k">fun</span> <span class="nf">someOtherPane</span><span class="p">()</span> <span class="p">:</span> <span class="nc">Region</span> <span class="p">=</span> <span class="nc">VBox</span><span class="p">().</span><span class="nf">apply</span><span class="p">{</span>
         <span class="p">.</span>
         <span class="p">.</span>
       <span class="kd">val</span> <span class="py">counter</span> <span class="p">:</span> <span class="nc">Int</span> <span class="p">=</span> <span class="mi">0</span>
       <span class="kd">val</span> <span class="py">glugLabel</span> <span class="p">=</span> <span class="nc">Label</span><span class="p">(</span><span class="s">"Glug...glug...glug..."</span><span class="p">)</span>
       <span class="kd">val</span> <span class="py">button</span> <span class="p">=</span> <span class="nc">Button</span><span class="p">(</span><span class="s">"Add one"</span><span class="p">)</span>
       <span class="n">button</span><span class="p">.</span><span class="nf">setOnAction</span> <span class="p">{</span> <span class="n">evt</span> <span class="p">-&gt;</span>
          <span class="n">countLabel</span><span class="p">.</span><span class="nf">setText</span><span class="p">(</span><span class="s">"You've had ${counter++} glasses."</span><span class="p">)</span>
          <span class="n">countPane</span><span class="p">.</span><span class="nf">getChildren</span><span class="p">().</span><span class="nf">setAll</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="o">..</span><span class="p">.,</span> <span class="n">glugLabel</span><span class="p">,</span> <span class="n">countLabel</span><span class="p">,</span> <span class="o">..</span><span class="p">.))</span>
       <span class="p">}</span>
       <span class="n">children</span> <span class="p">+=</span> <span class="n">button</span>
   <span class="p">}</span>    
</code></pre></div></div>
<p>Now, we need to know the contents of <code class="language-plaintext highlighter-rouge">countPane</code> in order to alter it without losing things already in it.  Any changes to <code class="language-plaintext highlighter-rouge">waterCounter()</code> will require changes to <code class="language-plaintext highlighter-rouge">someOtherPane()</code> as well.  Also, <code class="language-plaintext highlighter-rouge">countPane</code> has to be exposed as <em>at least</em> a <code class="language-plaintext highlighter-rouge">Pane</code> since <code class="language-plaintext highlighter-rouge">Region</code> doesn’t expose <code class="language-plaintext highlighter-rouge">getChildren()</code> publicly.</p>

<p>Even worse, in any implementation of this, we still have <code class="language-plaintext highlighter-rouge">button</code> reconfiguring <code class="language-plaintext highlighter-rouge">countLabel</code>.  Ideally, the only thing that configures <code class="language-plaintext highlighter-rouge">countLabel</code> should be <code class="language-plaintext highlighter-rouge">countLabel</code> itself, and the layout code that instantiates <code class="language-plaintext highlighter-rouge">countLabel</code>.</p>

<p>Finally, in any case where you have a reference to an object (<code class="language-plaintext highlighter-rouge">Node</code> or otherwise), that exposes the nature of its implementation, you have to check through the entire scope of that reference any time you contemplate making changes to that implementation.  In the first imperative example, the scope was just <code class="language-plaintext highlighter-rouge">waterCounter(){}</code>, but in the last example the scope was the entire layout building class.</p>

<p>In this lastest example, if you did decide to change <code class="language-plaintext highlighter-rouge">countLabel</code> to something else, maybe that sprite <code class="language-plaintext highlighter-rouge">ImageView</code> or a <code class="language-plaintext highlighter-rouge">Slider</code> or a pie chart, you’d have to make sure that no other part of the layout class uses the implementation of <code class="language-plaintext highlighter-rouge">countLabel</code>.  Maybe there’s some other <code class="language-plaintext highlighter-rouge">Node</code> that looks at the length of <code class="language-plaintext highlighter-rouge">countLabel.getText()</code> to determine some aspect of its layout.</p>

<h2 id="the-presentation-model-as-a-layout-builder-field">The Presentation Model as a Layout Builder Field</h2>

<p>In the first Reactive example, we had <code class="language-plaintext highlighter-rouge">counter</code> instantiated as a <code class="language-plaintext highlighter-rouge">StringProperty</code> variable reference local to <code class="language-plaintext highlighter-rouge">waterCounter(){}</code>.  If we move <code class="language-plaintext highlighter-rouge">button</code> somewhere else, then we’ll need to expand the scope of <code class="language-plaintext highlighter-rouge">counter</code> to a field in the layout builder class.  The code would look like this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="kd">val</span> <span class="py">counter</span> <span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
      <span class="p">.</span>
      <span class="p">.</span>
      <span class="p">.</span>
<span class="k">fun</span> <span class="nf">waterCounter</span><span class="p">()</span> <span class="p">:</span> <span class="nc">Region</span> <span class="p">=</span> <span class="nc">VBox</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
    <span class="n">children</span> <span class="p">+=</span> <span class="nc">Label</span><span class="p">(</span><span class="s">"Glug...glug...glug..."</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
       <span class="nf">visibleProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="n">counter</span><span class="p">.</span><span class="nf">greaterThan</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span>
       <span class="nf">managedProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="nf">visibleProperty</span><span class="p">())</span>
    <span class="p">}</span>
    <span class="n">children</span> <span class="p">+=</span> <span class="nc">Label</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
       <span class="nf">textProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="n">counter</span><span class="p">.</span><span class="nf">map</span><span class="p">{</span> <span class="s">"You've had $it glasses."</span> <span class="p">})</span>
    <span class="p">}</span>

<span class="p">}</span>
     <span class="p">.</span>
     <span class="p">.</span>
     <span class="p">.</span>
<span class="k">fun</span> <span class="nf">someOtherPane</span><span class="p">()</span> <span class="p">{</span>
         <span class="p">.</span>
         <span class="p">.</span>
         <span class="p">.</span>
    <span class="n">childern</span> <span class="p">+=</span> <span class="nc">Button</span><span class="p">(</span><span class="s">"Add one"</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="nf">setOnClick</span> <span class="p">{</span> <span class="n">evt</span> <span class="p">-&gt;</span> <span class="n">counter</span><span class="p">.</span><span class="n">value</span> <span class="p">+=</span> <span class="mi">1</span> <span class="p">}</span>
    <span class="p">}</span>         
         <span class="p">.</span>
         <span class="p">.</span>
         <span class="p">.</span>
<span class="p">}</span>     
</code></pre></div></div>
<p>You can see that <code class="language-plaintext highlighter-rouge">button</code> remains completely ignorant of the existence of the counter <code class="language-plaintext highlighter-rouge">Label</code>.</p>

<p>There still is a dependency - there has to be.  The counter <code class="language-plaintext highlighter-rouge">Label</code> is dependent on <code class="language-plaintext highlighter-rouge">counter</code>, and the <code class="language-plaintext highlighter-rouge">Button</code> knows about the nature of <code class="language-plaintext highlighter-rouge">counter</code>.</p>

<p>There are some things that make this a very different kind of coupling:</p>

<ol>
  <li>The nature of <code class="language-plaintext highlighter-rouge">counter</code> is very generic.<br />It is an <code class="language-plaintext highlighter-rouge">IntegerProperty</code>, which in the world of <code class="language-plaintext highlighter-rouge">Properties</code> is no more specific than declaring a regular variable as <code class="language-plaintext highlighter-rouge">Int</code>.</li>
  <li>Neither the counter <code class="language-plaintext highlighter-rouge">Label</code>, nor the <code class="language-plaintext highlighter-rouge">Button</code> is dependent on, even aware of the other.</li>
  <li>Neither the counter <code class="language-plaintext highlighter-rouge">Label</code>, nor the <code class="language-plaintext highlighter-rouge">Button</code> is aware of, or dependent on the implementation the other.</li>
  <li>Neither the counter <code class="language-plaintext highlighter-rouge">Label</code>, nor the <code class="language-plaintext highlighter-rouge">Button</code> is aware of the implementation of <code class="language-plaintext highlighter-rouge">counter</code>.</li>
  <li>Any other <code class="language-plaintext highlighter-rouge">Node</code> in the layout can interact with <code class="language-plaintext highlighter-rouge">counter</code> without impacting the implementation of either the <code class="language-plaintext highlighter-rouge">Button</code> or the counter <code class="language-plaintext highlighter-rouge">Label</code>.</li>
</ol>

<p>There has to be some understanding of the meaning of <code class="language-plaintext highlighter-rouge">counter</code> within some scope.  That scope might just be within the <code class="language-plaintext highlighter-rouge">waterCounter(){}</code> function, but it might be the entire layout builder class.  It’s hard to imagine any degree of coupling that is less than this which is still functional.</p>

<p>In a very important way, though, we have compartmentalized and isolated that coupling.  In this example, it’s <code class="language-plaintext highlighter-rouge">counter</code>, and we’ve essentially declared it the key <em>the</em> coupling point.  Any <code class="language-plaintext highlighter-rouge">Node</code> that we create that interacts with <code class="language-plaintext highlighter-rouge">counter</code> needs to share that understanding of the meaning of <code class="language-plaintext highlighter-rouge">counter</code>, and behave accordingly.  But it doesn’t have to understand how any other <code class="language-plaintext highlighter-rouge">Nodes</code> in the layout are interacting with <code class="language-plaintext highlighter-rouge">counter</code>.</p>

<h1 id="reactive-vs-imperative-design-in-a-framework">Reactive Vs Imperative Design in a Framework</h1>

<p>The goal of any framework, like MVC, MVP, MVVM or MVCI, is to reduce coupling.  Specifically, they are designed to provide a clear separation between the business/application logic and the layout.  This means that business logic works without any knowledge of the implementation of the layout, and the layout without any knowledge of the implementation of the business logic.</p>

<p>Let’s go back to our example…</p>

<h2 id="imperative-design-with-a-framework">Imperative Design With a Framework</h2>

<p>Imagine that whatever change needs to happen to <code class="language-plaintext highlighter-rouge">counter</code> is determined to be “application logic”, so it can no longer reside in the layout.  We’ll need to change the logic for the <code class="language-plaintext highlighter-rouge">Button</code> action.  Let’s look at a first try at the imperative design approach first:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="err">layout</span><span class="nc">Builder</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">counterChanger</span> <span class="p">:</span> <span class="p">(</span><span class="nc">Int</span><span class="p">,</span> <span class="p">(</span><span class="nc">Int</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Unit</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Unit</span><span class="p">)</span> <span class="p">{</span>
      <span class="p">.</span>
      <span class="p">.</span>
      <span class="p">.</span>
    <span class="k">fun</span> <span class="nf">someOtherPane</span><span class="p">()</span> <span class="p">:</span> <span class="nc">Region</span> <span class="p">=</span> <span class="nc">VBox</span><span class="p">().</span><span class="nf">apply</span><span class="p">{</span>
          <span class="p">.</span>
          <span class="p">.</span>
          <span class="p">.</span>
        <span class="kd">val</span> <span class="py">counter</span> <span class="p">:</span> <span class="nc">Int</span> <span class="p">=</span> <span class="mi">0</span>
        <span class="kd">val</span> <span class="py">glugLabel</span> <span class="p">=</span> <span class="nc">Label</span><span class="p">(</span><span class="s">"Glug...glug...glug..."</span><span class="p">)</span>
        <span class="kd">val</span> <span class="py">button</span> <span class="p">=</span> <span class="nc">Button</span><span class="p">(</span><span class="s">"Add one"</span><span class="p">)</span>
        <span class="n">button</span><span class="p">.</span><span class="nf">setOnAction</span> <span class="p">{</span> <span class="n">evt</span> <span class="p">-&gt;</span>
           <span class="n">counterChanger</span><span class="p">.</span><span class="nf">invoke</span><span class="p">(</span><span class="n">counter</span><span class="p">)</span> <span class="p">{</span> <span class="n">newCounter</span> <span class="p">-&gt;</span>
               <span class="n">counter</span> <span class="p">=</span> <span class="n">newCounter</span>
               <span class="n">countLabel</span><span class="p">.</span><span class="nf">setText</span><span class="p">(</span><span class="s">"You've had ${counter} glasses."</span><span class="p">)</span>
               <span class="k">if</span> <span class="p">(</span><span class="n">counter</span> <span class="p">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
                   <span class="n">countPane</span><span class="p">.</span><span class="nf">getChildren</span><span class="p">().</span><span class="nf">setAll</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="o">..</span><span class="p">.,</span> <span class="n">glugLabel</span><span class="p">,</span> <span class="n">countLabel</span><span class="p">,</span> <span class="o">..</span><span class="p">.))</span>
               <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                   <span class="n">countPane</span><span class="p">.</span><span class="nf">getChildren</span><span class="p">().</span><span class="nf">setAll</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="o">..</span><span class="p">.,</span> <span class="n">countLabel</span><span class="p">,</span> <span class="o">..</span><span class="p">.))</span>
               <span class="p">}</span>
           <span class="p">}</span>
        <span class="p">}</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="n">button</span>
    <span class="p">}</span>  
<span class="p">}</span>
</code></pre></div></div>
<p>Let’s look at that <code class="language-plaintext highlighter-rouge">(Int, (Int) -&gt; Unit) -&gt; Unit</code> first.  This is the equivalent of the Java, <code class="language-plaintext highlighter-rouge">BiConsumer&lt;Integer, Consumer&lt;Integer&gt;&gt;</code>.  It’s a functional element that accepts an Integer and another function that also accepts an Integer.</p>

<p>We don’t know what <code class="language-plaintext highlighter-rouge">counterChanger</code> does.  Maybe it calls a website or some other external API, either of which would block the FXAT.  So <code class="language-plaintext highlighter-rouge">counterChanger</code> might use <code class="language-plaintext highlighter-rouge">Task</code> to run the processing on a background thread.  That means that we cannot just use <code class="language-plaintext highlighter-rouge">(Int) -&gt; Int</code>, or <code class="language-plaintext highlighter-rouge">Function&lt;Integer, Integer&gt;</code> because we can’t wait for the answer to come back.  Instead, we define a <code class="language-plaintext highlighter-rouge">Consumer</code> that will handle the answer when it comes back, and we send that off along with the current value of <code class="language-plaintext highlighter-rouge">counter</code>.</p>

<p>We also cannot assume that <code class="language-plaintext highlighter-rouge">counter</code> is going to increase through <code class="language-plaintext highlighter-rouge">counterChanger</code>.  So we can’t just automatically add <code class="language-plaintext highlighter-rouge">glugLabel</code> when <code class="language-plaintext highlighter-rouge">button</code> is clicked.  So we’ll need some logic to decide that.</p>

<p>You can see, though, that we still have a lot of coupling between <code class="language-plaintext highlighter-rouge">button</code> and the application logic that deals with <code class="language-plaintext highlighter-rouge">counter</code>.  For instance, we know that it requires the current value of <code class="language-plaintext highlighter-rouge">counter</code> and nothing else.  What if there were other factors involved?  How about <code class="language-plaintext highlighter-rouge">Spinner</code> somewhere on the screen with a maximum value for <code class="language-plaintext highlighter-rouge">counter</code>?  How about a <code class="language-plaintext highlighter-rouge">CheckBox</code> or <code class="language-plaintext highlighter-rouge">ToggleButton</code> that determined if the <code class="language-plaintext highlighter-rouge">counter</code> is incremented or decremented.  Or something else that determined by how much?  What if <code class="language-plaintext highlighter-rouge">counterChanger</code> also needs to reset that “how much” value back to 1 each time?  All of this would change how <code class="language-plaintext highlighter-rouge">button</code> was implemented.</p>

<p>Let’s look at the next step towards solving this…</p>

<h3 id="the-presentation-model-in-the-framework">The Presentation Model in the Framework</h3>

<p>Any of the frameworks - MVC, MVVM or MVCI - is going to have the Presentation Model represented somewhere.  In MVC, it’s part of the “Model”, in MVVM it’s in the “ViewModel” and in MVCI it <em>is</em> the “Model” component.  For the sake of simplicity, let’s assume that we’ve designed our framework implementation such that we can get the Presentation Model with a single method call, even if we are using MVC, or MVVM.  That would be <code class="language-plaintext highlighter-rouge">model.getPresentationModel()</code> or <code class="language-plaintext highlighter-rouge">viewModel.getPresentationModel()</code>.</p>

<p>Once we do that, we can assume that all of the components of the framework have easy access to the Presentation Model.</p>

<p>In this case, we would supply the Presentation Model to the layout builder class somehow, probably as a constructor parameter.  But, because the Presentation Model is now shared, we don’t need to pass <code class="language-plaintext highlighter-rouge">counter</code> around any more.  So we get this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="err">layout</span><span class="nc">Builder</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">presentationModel</span> <span class="p">:</span> <span class="nc">PresentationModel</span><span class="p">,</span>
                    <span class="k">private</span> <span class="kd">val</span> <span class="py">counterChanger</span> <span class="p">:</span> <span class="p">(()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Unit</span><span class="p">)</span> <span class="p">{</span>
      <span class="p">.</span>
      <span class="p">.</span>
      <span class="p">.</span>
    <span class="k">fun</span> <span class="nf">someOtherPane</span><span class="p">()</span> <span class="p">:</span> <span class="nc">Region</span> <span class="p">=</span> <span class="nc">VBox</span><span class="p">().</span><span class="nf">apply</span><span class="p">{</span>
          <span class="p">.</span>
          <span class="p">.</span>
          <span class="p">.</span>
        <span class="kd">val</span> <span class="py">glugLabel</span> <span class="p">=</span> <span class="nc">Label</span><span class="p">(</span><span class="s">"Glug...glug...glug..."</span><span class="p">)</span>
        <span class="kd">val</span> <span class="py">button</span> <span class="p">=</span> <span class="nc">Button</span><span class="p">(</span><span class="s">"Add one"</span><span class="p">)</span>
        <span class="n">button</span><span class="p">.</span><span class="nf">setOnAction</span> <span class="p">{</span> <span class="n">evt</span> <span class="p">-&gt;</span>
           <span class="n">counterChanger</span><span class="p">.</span><span class="nf">invoke</span> <span class="p">{</span>
               <span class="n">countLabel</span><span class="p">.</span><span class="nf">setText</span><span class="p">(</span><span class="s">"You've had ${presentationModel.counter} glasses."</span><span class="p">)</span>
               <span class="k">if</span> <span class="p">(</span><span class="n">presentationModel</span><span class="p">.</span><span class="n">counter</span> <span class="p">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
                   <span class="n">countPane</span><span class="p">.</span><span class="nf">getChildren</span><span class="p">().</span><span class="nf">setAll</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="o">..</span><span class="p">.,</span> <span class="n">glugLabel</span><span class="p">,</span> <span class="n">countLabel</span><span class="p">,</span> <span class="o">..</span><span class="p">.))</span>
               <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                   <span class="n">countPane</span><span class="p">.</span><span class="nf">getChildren</span><span class="p">().</span><span class="nf">setAll</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="o">..</span><span class="p">.,</span> <span class="n">countLabel</span><span class="p">,</span> <span class="o">..</span><span class="p">.))</span>
               <span class="p">}</span>
           <span class="p">}</span>
        <span class="p">}</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="n">button</span>
    <span class="p">}</span>  
<span class="p">}</span>
</code></pre></div></div>
<p>Whatever <code class="language-plaintext highlighter-rouge">counterChanger</code> does, it’s impact is to modify <code class="language-plaintext highlighter-rouge">counter</code> inside <code class="language-plaintext highlighter-rouge">presentationModel</code>.  Now, <code class="language-plaintext highlighter-rouge">counterChanger</code> is essentially a <code class="language-plaintext highlighter-rouge">Consumer&lt;Runnable&gt;</code>.</p>

<p>What does <code class="language-plaintext highlighter-rouge">counterChanger</code> do?</p>

<p>We don’t know the specifics, but there is an understanding that it will “change <code class="language-plaintext highlighter-rouge">PresentationModel.counter</code>”.  Since it’s defined as a <code class="language-plaintext highlighter-rouge">Consumer</code> of a <code class="language-plaintext highlighter-rouge">Runnable</code>, it will execute that <code class="language-plaintext highlighter-rouge">Runnable</code> when it has finished updating <code class="language-plaintext highlighter-rouge">PresentationModel.counter</code>.</p>

<p>Let’s look at one of those, “What if?” questions from the end of the last section.  The “How much?” question, and the idea of resetting it after each call to <code class="language-plaintext highlighter-rouge">counterChanger</code>.  We’ll assume we have a <code class="language-plaintext highlighter-rouge">Spinner&lt;Int&gt;</code> somewhere in our layout, and it’s called <code class="language-plaintext highlighter-rouge">howMuchSpinner</code>.   We’ll also add a value into our Presentation Model:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">PresentationModel</span> <span class="p">{</span>
   <span class="kd">var</span> <span class="py">counter</span> <span class="p">:</span> <span class="nc">Int</span> <span class="p">=</span> <span class="mi">0</span>
   <span class="kd">var</span> <span class="py">howMuch</span> <span class="p">:</span> <span class="nc">Int</span> <span class="p">=</span> <span class="mi">1</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Now, this is what our layout code looks like:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="err">layout</span><span class="nc">Builder</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">presentationModel</span> <span class="p">:</span> <span class="nc">PresentationModel</span><span class="p">,</span>
                    <span class="k">private</span> <span class="kd">val</span> <span class="py">counterChanger</span> <span class="p">:</span> <span class="p">(()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Unit</span><span class="p">)</span> <span class="p">{</span>
      <span class="p">.</span>
      <span class="p">.</span>
      <span class="p">.</span>
    <span class="k">fun</span> <span class="nf">someOtherPane</span><span class="p">()</span> <span class="p">:</span> <span class="nc">Region</span> <span class="p">=</span> <span class="nc">VBox</span><span class="p">().</span><span class="nf">apply</span><span class="p">{</span>
          <span class="p">.</span>
          <span class="p">.</span>
          <span class="p">.</span>
        <span class="kd">val</span> <span class="py">glugLabel</span> <span class="p">=</span> <span class="nc">Label</span><span class="p">(</span><span class="s">"Glug...glug...glug..."</span><span class="p">)</span>
        <span class="kd">val</span> <span class="py">button</span> <span class="p">=</span> <span class="nc">Button</span><span class="p">(</span><span class="s">"Add one"</span><span class="p">)</span>
        <span class="n">button</span><span class="p">.</span><span class="nf">setOnAction</span> <span class="p">{</span> <span class="n">evt</span> <span class="p">-&gt;</span>
           <span class="n">presentationModel</span><span class="p">.</span><span class="n">howMuch</span> <span class="p">=</span> <span class="n">howMuchSpinner</span><span class="p">.</span><span class="nf">getValue</span><span class="p">()</span>
           <span class="n">counterChanger</span><span class="p">.</span><span class="nf">invoke</span> <span class="p">{</span>
               <span class="n">howMuchSpinner</span><span class="p">.</span><span class="nf">setValue</span><span class="p">(</span><span class="n">presentationModel</span><span class="p">.</span><span class="n">howMuch</span><span class="p">)</span>
               <span class="n">countLabel</span><span class="p">.</span><span class="nf">setText</span><span class="p">(</span><span class="s">"You've had ${presentationModel.counter} glasses."</span><span class="p">)</span>
               <span class="k">if</span> <span class="p">(</span><span class="n">presentationModel</span><span class="p">.</span><span class="n">counter</span> <span class="p">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
                   <span class="n">countPane</span><span class="p">.</span><span class="nf">getChildren</span><span class="p">().</span><span class="nf">setAll</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="o">..</span><span class="p">.,</span> <span class="n">glugLabel</span><span class="p">,</span> <span class="n">countLabel</span><span class="p">,</span> <span class="o">..</span><span class="p">.))</span>
               <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                   <span class="n">countPane</span><span class="p">.</span><span class="nf">getChildren</span><span class="p">().</span><span class="nf">setAll</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="o">..</span><span class="p">.,</span> <span class="n">countLabel</span><span class="p">,</span> <span class="o">..</span><span class="p">.))</span>
               <span class="p">}</span>
           <span class="p">}</span>
        <span class="p">}</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="n">button</span>
    <span class="p">}</span>  
<span class="p">}</span>
</code></pre></div></div>
<p>Our <code class="language-plaintext highlighter-rouge">button</code> still doesn’t know exactly <em>what</em> <code class="language-plaintext highlighter-rouge">counterChanger</code> does.  But it does need to know that its implementation requires <code class="language-plaintext highlighter-rouge">howMuch</code> and that it might change <code class="language-plaintext highlighter-rouge">howMuch</code>.  It also needs to know that there is a <code class="language-plaintext highlighter-rouge">Spinner</code> called <code class="language-plaintext highlighter-rouge">howMuchSpinner</code> and that it needs to extract its value to update <code class="language-plaintext highlighter-rouge">presentationModel</code> before it invokes <code class="language-plaintext highlighter-rouge">counterChanger</code>.</p>

<p>Unfortunately, this is the end of the road for the Imperative approach.  As long as the “State” of the GUI is stored inside the <code class="language-plaintext highlighter-rouge">Nodes</code> that make up the layout, there is no way to avoid building the logic to pick the pieces that you need out of those <code class="language-plaintext highlighter-rouge">Nodes</code> and updating the Presentation Model.  And that, all by itself, demands that the layout logic is coupled to the implementation of the application logic.</p>

<p>But, you ask…What if you just always copy all of the <code class="language-plaintext highlighter-rouge">Node</code> data into the Presentation Model before any business logic call, and then copy it all back into the <code class="language-plaintext highlighter-rouge">Nodes</code> afterwards???</p>

<p>Well, if you’re going to do that, you might as well go with a Reactive design.  Let’s look at how that would work:</p>

<h2 id="reactive-design-framework">Reactive Design Framework</h2>

<p>We’ll start out by looking at the Presentation Model:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">PresentationModel</span> <span class="p">{</span>
   <span class="kd">val</span> <span class="py">counter</span> <span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
   <span class="kd">val</span> <span class="py">howMuch</span> <span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Not that exciting, we’ve just changed the two fields over to <code class="language-plaintext highlighter-rouge">IntegerProperty</code>.  Here’s our layout code:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="err">layout</span><span class="nc">Builder</span><span class="p">(</span><span class="kd">val</span> <span class="py">presentationModel</span> <span class="p">:</span> <span class="nc">PresentationModel</span><span class="p">,</span>
                    <span class="kd">val</span> <span class="py">counterUpdater</span> <span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span><span class="p">)</span> <span class="p">{</span>
      <span class="p">.</span>
      <span class="p">.</span>
      <span class="p">.</span>
    <span class="k">fun</span> <span class="nf">waterCounter</span><span class="p">()</span> <span class="p">:</span> <span class="nc">Region</span> <span class="p">=</span> <span class="nc">VBox</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nc">Label</span><span class="p">(</span><span class="s">"Glug...glug...glug..."</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
           <span class="nf">visibleProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="n">presentationModel</span><span class="p">.</span><span class="n">counter</span><span class="p">.</span><span class="nf">greaterThan</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span>
           <span class="nf">managedProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="nf">visibleProperty</span><span class="p">())</span>
        <span class="p">}</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nc">Label</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
           <span class="nf">textProperty</span><span class="p">().</span><span class="nf">bind</span><span class="p">(</span><span class="n">presentationModel</span><span class="p">.</span><span class="n">counter</span><span class="p">.</span><span class="nf">map</span><span class="p">{</span> <span class="s">"You've had $it glasses."</span> <span class="p">})</span>
        <span class="p">}</span>
    <span class="p">}</span>
         <span class="p">.</span>
         <span class="p">.</span>
         <span class="p">.</span>
    <span class="k">fun</span> <span class="nf">someOtherPane</span><span class="p">()</span> <span class="p">{</span>
             <span class="p">.</span>
             <span class="p">.</span>
             <span class="p">.</span>
        <span class="n">children</span> <span class="p">+=</span> <span class="nc">Button</span><span class="p">(</span><span class="s">"Add one"</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
            <span class="nf">setOnClick</span> <span class="p">{</span> <span class="n">evt</span> <span class="p">-&gt;</span> <span class="n">counterUpdater</span><span class="p">.</span><span class="nf">invoke</span><span class="p">()</span> <span class="p">}</span>
        <span class="p">}</span>         
             <span class="p">.</span>
             <span class="p">.</span>
             <span class="p">.</span>
    <span class="p">}</span>     
<span class="p">}</span>
</code></pre></div></div>
<p>Hmmmm…that doesn’t look different from the last Reactive version.  We now have <code class="language-plaintext highlighter-rouge">presentationModel</code> passed in as constructor parameter and <code class="language-plaintext highlighter-rouge">counter</code> has been removed as a stand-alone field.  References to <code class="language-plaintext highlighter-rouge">counter</code> have been changed to <code class="language-plaintext highlighter-rouge">presentationModel.counter</code>.</p>

<p>The only other change is that the <code class="language-plaintext highlighter-rouge">Button</code> event now calls the <code class="language-plaintext highlighter-rouge">counterUpdater</code> which, in this version, is the Kotlin equivalent to <code class="language-plaintext highlighter-rouge">Runnable</code>.  We don’t need to pass anything to it, because everything is already synchronized with <code class="language-plaintext highlighter-rouge">presentationModel</code>, which the application logic has access to.</p>

<p>Note that the <code class="language-plaintext highlighter-rouge">Button</code> logic does <strong>nothing</strong> except invoke <code class="language-plaintext highlighter-rouge">counterUpdater</code>.  It has no need to know about any other <code class="language-plaintext highlighter-rouge">Nodes</code> in the layout, nor does it have any need to know <strong>anything</strong> about the implementation of <code class="language-plaintext highlighter-rouge">counterUpdater</code>.</p>

<p>You’ll also note that I haven’t put in any code at all that deals with <code class="language-plaintext highlighter-rouge">PresentationModel.howMuch</code>.  Presumably, there’s some kind of <code class="language-plaintext highlighter-rouge">Node</code>, or <code class="language-plaintext highlighter-rouge">Nodes</code> that synchronize with <code class="language-plaintext highlighter-rouge">PresentationModel.howMuch</code>, but they are not relevant to this discussion at all.</p>

<p>None of anything that we are discussing here is coupled to the implementation of <code class="language-plaintext highlighter-rouge">PresentationModel.howMuch</code> at all.</p>

<h3 id="this-approach-breaks-mvc">This Approach “Breaks” MVC</h3>

<p>In Model-View-Controller there is a rule that says that the View is able to read the Presentation Model from the Model, but all changes to the Presentation Model <strong>must</strong> go through the Controller.</p>

<p>Right there, you cannot build a Reactive system with MVC.  Because a Reactive system demands that the UI can update State, and this is explicitly forbidden in MVC.  From a JavaFX perspective, this means that you cannot bidirectionally bind the Presentation Model to the layout <code class="language-plaintext highlighter-rouge">Nodes</code>, nor can you bind unidirectionally from a <code class="language-plaintext highlighter-rouge">Node</code> to the Presentation Model.</p>

<p>Should you care?</p>

<p>Probably not.  But, if you do ignore this rule, and if you implement your Presentation Model as an enclosing object around your State <code class="language-plaintext highlighter-rouge">Properties</code> (even if it is still contained within the Model), then you’re 90% of the way from MVC to MVCI.  And you might as well use MVCI - it’s better for this stuff anyways.</p>

<h2 id="application-logic-in-a-reactive-design">Application Logic in a Reactive Design</h2>

<p>Let’s look at the other end of the framework, the application logic…</p>

<p>In MVC and MVVM, the application logic is in the Model.  In MVCI the application logic is in the Interactor.  MVC and MVCI are very similar in that application logic in both has access to the Presentation Model and can freely read and write from and to it.  MVVM is different in that the Presentation Model is in the ViewModel and the Model doesn’t have open access to its elements.  But that’s a different issue, and we can’t go into it here.  So we’ll concentrate on MVCI, with the understanding that much of it applies to MVC as well.</p>

<p>Typically, in MVCI you would pass a reference to the Presentation Model to the Interactor as a constructor parameter so that the Interactor can access it freely.  Let’s take a look at how we might implement our <code class="language-plaintext highlighter-rouge">counterUpdater</code> logic:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">Interactor</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">model</span> <span class="p">:</span> <span class="nc">PresentationModel</span><span class="p">)</span>  <span class="p">{</span>

    <span class="k">fun</span> <span class="nf">updateCounter</span><span class="p">()</span> <span class="p">:</span> <span class="nc">Int</span> <span class="p">{</span>
       <span class="k">return</span> <span class="nf">someService</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">counter</span><span class="p">.</span><span class="n">value</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">howMuch</span><span class="p">.</span><span class="n">value</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">fun</span> <span class="nf">newCounterValue</span><span class="p">(</span><span class="n">newVal</span> <span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">{</span>
       <span class="n">model</span><span class="p">.</span><span class="n">counter</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="n">newVal</span>
       <span class="n">model</span><span class="p">.</span><span class="n">howMuch</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="mi">1</span>
    <span class="p">}</span>

<span class="p">}</span>
</code></pre></div></div>
<p>For context, this would be called from something like this, in the Controller:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">updateCounter</span><span class="p">()</span> <span class="p">{</span>
   <span class="kd">val</span> <span class="py">task</span> <span class="p">:</span> <span class="nc">Task</span><span class="p">&lt;</span><span class="nc">Int</span><span class="p">&gt;</span> <span class="p">=</span> <span class="kd">object</span> <span class="err">: </span><span class="nc">Task</span><span class="p">()</span> <span class="p">{</span>
      <span class="k">override</span> <span class="k">fun</span> <span class="nf">call</span><span class="p">()</span> <span class="p">:</span> <span class="nc">Int</span> <span class="p">{</span>
         <span class="k">return</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">updateCounter</span><span class="p">()</span>
      <span class="p">}</span>
   <span class="p">}</span>
   <span class="n">task</span><span class="p">.</span><span class="nf">setOnSucceeded</span> <span class="p">{</span><span class="n">evt</span> <span class="p">-&gt;</span> <span class="n">interactor</span><span class="p">.</span><span class="nf">newCounterValue</span><span class="p">(</span><span class="n">task</span><span class="p">.</span><span class="k">get</span><span class="p">())}</span>
   <span class="nc">Thread</span><span class="p">(</span><span class="n">task</span><span class="p">).</span><span class="nf">start</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>
<p>If you are familiar with <code class="language-plaintext highlighter-rouge">Task</code> then you’ll see that <code class="language-plaintext highlighter-rouge">Interactor.updateCounter()</code> is called on the background thread, and <code class="language-plaintext highlighter-rouge">Interactor.newCounterValue()</code> is called from FXAT.  This latter method, therefore, is allowed to update <code class="language-plaintext highlighter-rouge">Properties</code> bound to the Scene Graph.  You should also notice, that the Interactor doesn’t have any explicit exposure to the threads involved, even though it is logically divided up into methods that can or cannot be run on the FXAT.</p>

<h3 id="exposing-the-properties-to-the-interactor">Exposing the Properties to the Interactor</h3>

<p>You might have noticed that in our Interactor we have lines like this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">model</span><span class="p">.</span><span class="n">counter</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="n">newVal</span>
</code></pre></div></div>
<p>Clearly, to do this, the Interactor has to be aware of the <code class="language-plaintext highlighter-rouge">Property</code> nature of <code class="language-plaintext highlighter-rouge">model.counter</code> or, in other words, the Interactor is dependent on the implementation of <code class="language-plaintext highlighter-rouge">model.counter</code> as a <code class="language-plaintext highlighter-rouge">Property</code>.  Can we limit this?</p>

<p>You can.  You can create an <code class="language-plaintext highlighter-rouge">Interface</code> that essentially has the getters and setters for the values of the <code class="language-plaintext highlighter-rouge">Properties</code> and then pass the Presentation Model to the Interactor as that <code class="language-plaintext highlighter-rouge">Interface</code>.  Like this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">InteractorModel</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="py">counter</span><span class="p">:</span> <span class="nc">Int</span>
    <span class="kd">var</span> <span class="py">howMuch</span><span class="p">:</span> <span class="nc">Int</span>
<span class="p">}</span>

<span class="kd">class</span> <span class="nc">PresentationModel</span> <span class="p">:</span> <span class="nc">InteractorModel</span> <span class="p">{</span>

    <span class="kd">val</span> <span class="py">counterProperty</span><span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">howMuchProperty</span><span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>

    <span class="k">override</span> <span class="kd">var</span> <span class="py">counter</span><span class="p">:</span> <span class="nc">Int</span>
        <span class="k">get</span><span class="p">()</span> <span class="p">=</span> <span class="n">counterProperty</span><span class="p">.</span><span class="k">get</span><span class="p">()</span>
        <span class="k">set</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="p">=</span> <span class="n">counterProperty</span><span class="p">.</span><span class="k">set</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>

    <span class="k">override</span> <span class="kd">var</span> <span class="py">howMuch</span><span class="p">:</span> <span class="nc">Int</span>
        <span class="k">get</span><span class="p">()</span> <span class="p">=</span> <span class="n">howMuchProperty</span><span class="p">.</span><span class="k">get</span><span class="p">()</span>
        <span class="k">set</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="p">=</span> <span class="n">howMuchProperty</span><span class="p">.</span><span class="k">set</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This looks a bit different in Kotlin.  We’ve defined <code class="language-plaintext highlighter-rouge">Int</code> fields in the <code class="language-plaintext highlighter-rouge">Interface</code>, which is allowed in Kotlin.  Then we’ve implemented these fields in <code class="language-plaintext highlighter-rouge">PresentationModel</code> such that they delegate their <code class="language-plaintext highlighter-rouge">set()</code> and <code class="language-plaintext highlighter-rouge">get()</code> methods to the <code class="language-plaintext highlighter-rouge">get()</code> and <code class="language-plaintext highlighter-rouge">set()</code> methods of the respective <code class="language-plaintext highlighter-rouge">IntegerProperties</code>.  Now, we can do this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">Interactor</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">model</span><span class="p">:</span> <span class="nc">InteractorModel</span><span class="p">)</span> <span class="p">{</span>

    <span class="k">fun</span> <span class="nf">updateCounter</span><span class="p">()</span> <span class="p">:</span> <span class="nc">Int</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nf">someService</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">counter</span><span class="p">,</span> <span class="n">model</span><span class="p">.</span><span class="n">howMuch</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">fun</span> <span class="nf">newCounterValue</span><span class="p">(</span><span class="n">newVal</span> <span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">model</span><span class="p">.</span><span class="n">counter</span> <span class="p">=</span> <span class="n">newVal</span>
        <span class="n">model</span><span class="p">.</span><span class="n">howMuch</span><span class="p">=</span> <span class="mi">1</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And the Interactor can only interact with <code class="language-plaintext highlighter-rouge">model.counter</code> and <code class="language-plaintext highlighter-rouge">model.howMuch</code> as <code class="language-plaintext highlighter-rouge">Int</code>.  The layout code, would now use bindings to <code class="language-plaintext highlighter-rouge">model.counterProperty</code> and <code class="language-plaintext highlighter-rouge">model.howMuchProperty</code>.</p>

<p>But what if you want to prevent the layout from changing <code class="language-plaintext highlighter-rouge">model.counter</code>?  That actually seems like a reasonable requirement.  We could expose <code class="language-plaintext highlighter-rouge">model.counterProperty</code> as <code class="language-plaintext highlighter-rouge">ObservableIntegerValue</code>, but the layout is still going to be able to update <code class="language-plaintext highlighter-rouge">model.counter</code>.  How do we prevent that?</p>

<p>We just do the same thing as we did for the Interactor, except instead of exposing the getters and setters, expose only the <code class="language-plaintext highlighter-rouge">Properties</code>:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">LayoutModel</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">counterProperty</span><span class="p">:</span> <span class="nc">ReadOnlyIntegerProperty</span>
    <span class="kd">val</span> <span class="py">howMuchProperty</span><span class="p">:</span> <span class="nc">IntegerProperty</span>
<span class="p">}</span>

<span class="kd">class</span> <span class="nc">PresentationModel</span> <span class="p">:</span> <span class="nc">InteractorModel</span><span class="p">,</span> <span class="nc">LayoutModel</span> <span class="p">{</span>

    <span class="k">private</span> <span class="kd">val</span> <span class="py">counterPropertyImpl</span><span class="p">:</span> <span class="nc">ReadOnlyIntegerWrapper</span> <span class="p">=</span> <span class="nc">ReadOnlyIntegerWrapper</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
    <span class="k">override</span> <span class="kd">val</span> <span class="py">counterProperty</span><span class="p">:</span> <span class="nc">ReadOnlyIntegerProperty</span> <span class="p">=</span> <span class="n">counterPropertyImpl</span><span class="p">.</span><span class="n">readOnlyProperty</span>
    <span class="k">override</span> <span class="kd">val</span> <span class="py">howMuchProperty</span><span class="p">:</span> <span class="nc">IntegerProperty</span> <span class="p">=</span> <span class="nc">SimpleIntegerProperty</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>

    <span class="k">override</span> <span class="kd">var</span> <span class="py">counter</span><span class="p">:</span> <span class="nc">Int</span>
        <span class="k">get</span><span class="p">()</span> <span class="p">=</span> <span class="n">counterPropertyImpl</span><span class="p">.</span><span class="k">get</span><span class="p">()</span>
        <span class="k">set</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="p">=</span> <span class="n">counterPropertyImpl</span><span class="p">.</span><span class="k">set</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>

    <span class="k">override</span> <span class="kd">var</span> <span class="py">howMuch</span><span class="p">:</span> <span class="nc">Int</span>
        <span class="k">get</span><span class="p">()</span> <span class="p">=</span> <span class="n">howMuchProperty</span><span class="p">.</span><span class="k">get</span><span class="p">()</span>
        <span class="k">set</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="p">=</span> <span class="n">howMuchProperty</span><span class="p">.</span><span class="k">set</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>I’ve called the new <code class="language-plaintext highlighter-rouge">Interface LayoutModel</code> because <code class="language-plaintext highlighter-rouge">ViewModel</code> would get confusing.  This <code class="language-plaintext highlighter-rouge">Interface</code> exposes <code class="language-plaintext highlighter-rouge">howMuchProperty</code> as <code class="language-plaintext highlighter-rouge">IntegerProperty</code> and <code class="language-plaintext highlighter-rouge">counterProperty</code> as <code class="language-plaintext highlighter-rouge">ReadOnlyIntegerProperty</code>.  In the <code class="language-plaintext highlighter-rouge">PresentationModel</code> we now have a private <code class="language-plaintext highlighter-rouge">IntegerProperty</code> instantiated as <code class="language-plaintext highlighter-rouge">ReadOnlyIntegerWrapper</code>.  Then we implement the <code class="language-plaintext highlighter-rouge">Interface</code> by instantiating <code class="language-plaintext highlighter-rouge">counterProperty</code> with <code class="language-plaintext highlighter-rouge">counterPropertyImpl.getReadOnlyProperty()</code>.</p>

<p>Obviously, we pass <code class="language-plaintext highlighter-rouge">PresentationModel</code> to the layout builder class as <code class="language-plaintext highlighter-rouge">LayoutModel</code> and the layout builder only has acces to the fields as <code class="language-plaintext highlighter-rouge">Properties</code>, and <code class="language-plaintext highlighter-rouge">counter</code> is read-only.</p>

<p>The result is that the application logic treats the Presentation Model like set of normal Java types and objects, while the layout treats the same Presentation Model like a set of <code class="language-plaintext highlighter-rouge">Observable</code> objects that it can connect to the layout through <code class="language-plaintext highlighter-rouge">Bindings</code> and <code class="language-plaintext highlighter-rouge">Listeners</code>.  In the end, the application logic performs, <code class="language-plaintext highlighter-rouge">model.counter = newVal</code> and the <code class="language-plaintext highlighter-rouge">Label</code> on the screen instantly changes.</p>

<h1 id="conclusion">Conclusion</h1>

<p>I think that JavaFX gets missed as a Reactive GUI environment because it’s different from the other, commonly used systems like React.</p>

<p>I spent a bit of time looking through the results of a web search for “JavaFX reactive”, and it wasn’t very encouraging.  Most of the results seemed to deal with integrating Reactive tools like RxJava with JavaFX.  This is an interesting subject, but only tangential to Reactive GUI design.  Those results that did deal with JavaFX as a Reactive GUI environment consisted of superficial surveys of a few <code class="language-plaintext highlighter-rouge">Observable</code> classes and not much else.</p>

<p>It’s almost disappointing that you <em>can</em> build a working application using a declarative/imperative approach to GUI design with JavaFX, because that’s as far as most people seem to get.  Add FXML into the mix, and Reactive design gets pushed even further into the background.  I don’t think I’ve ever seen an example of an FXML based Reactive JavaFX application.  They’re probably out there, but nobody seems to be talking about it.</p>

<p>It’s a shame, because, like so many things with JavaFX, once you know how to do it properly it becomes so much easier than doing it the “wrong” way.  Reactive GUI applications in JavaFX are really that much simpler than declarative/imperative designs, have so much less coupling - all over the application - and are easier to maintain and enhance.</p>

<p>When you integrate a Reactive design into MVCI (which is what it is designed for) you end up with a Presentation Model that is treated as a collection of <code class="language-plaintext highlighter-rouge">Observables</code> for binding at the layout end of the application, and treated as a collection of generic Java/Kotlin types and objects at the application logic.  That Presentation Model becomes a conduit for each end of the application - View and Application Logic - to communicate with each other in the manner that makes the most sense to them, <strong>without knowing about each other.</strong>  That allows them to interact without becoming coupled.</p>]]></content><author><name>Dave Barrett</name><email>pragmaticcoding8@gmail.com</email></author><category term="javafx" /><summary type="html"><![CDATA[What is Reactive programming, and how does it work in JavaFX]]></summary></entry></feed>