Property Change Event Firing when property not changed

If I try to set a builtin property on a widget to the same value it has, the propertyChange event does not fire.
If I try to set a builtin property on a widget to python’s None, the type check fails and nothing happens.

If I try to set a custom property on a widget to the same value it has, the propertyChange event does not fire.
If I try to set a custom property on a widget to python’s None, the propertyChange event fires even if the property’s value was already None.

I’ve added checks for None to prevent creating an infinite loop like the example below does, but I’m guessing this is not intended behavior?

Here is some xml you can paste into a vision window that does a proof of concept.


<?xml version="1.0" encoding="UTF-8"?>
<objects fpmi.archive.type="components" framework.version="8.1.14.2022012711" com.inductiveautomation.vision.version="11.1.14.0" timestamp="Wed Feb 09 15:10:12 CST 2022">
	<arraylist len="6">
		<o cls="com.inductiveautomation.factorypmi.application.binding.action.ActionAdapter">
			<o-c m="setEventSet" s="1;java.beans.EventSetDescriptor">
				<o id="1" cls="java.beans.EventSetDescriptor">
					<o-ctor s="5;str;java.lang.Class;[java.beans.MethodDescriptor;java.lang.reflect.Method;java.lang.reflect.Method">
						<str>action</str>
						<class>java.awt.event.ActionListener</class>
						<array cls="java.beans.MethodDescriptor" len="1">
							<o id="0" cls="java.beans.MethodDescriptor">
								<o-ctor s="2;java.lang.reflect.Method;[java.beans.ParameterDescriptor">
									<method>java.awt.event.ActionListener|actionPerformed|1;java.awt.event.ActionEvent</method>
									<null/>
								</o-ctor>
							</o>
						</array>
						<method>javax.swing.AbstractButton|addActionListener|1;java.awt.event.ActionListener</method>
						<method>javax.swing.AbstractButton|removeActionListener|1;java.awt.event.ActionListener</method>
					</o-ctor>
				</o>
			</o-c>
			<o-c m="setJythonCode" s="1;str"><str>event.source.parent.getComponent(&apos;Numeric Text Field 1&apos;).intValue = 0
event.source.parent.getComponent(&apos;Numeric Text Field&apos;).intValue = 0
event.source.parent.getComponent(&apos;Numeric Text Field 1&apos;).myCustomProp = 0</str></o-c>
			<o-c m="setMethodDescriptor" s="1;java.beans.MethodDescriptor"><ref>0</ref></o-c>
			<o-c m="setScopeStyle" s="1;com.inductiveautomation.ignition.common.script.ScriptScopeStyle"><enum id="2" cls="com.inductiveautomation.ignition.common.script.ScriptScopeStyle" n="Python25"/></o-c>
			<o-c m="setTarget" s="1;java.awt.Component">
				<c id="9" cls="com.inductiveautomation.factorypmi.application.components.PMIButton">
					<c-comm>
						<p2df>71.0;27.0</p2df>
						<r2dd>376.0;300.0;71.0;27.0</r2dd>
						<str>Button</str>
						<lc>376.0;300.0;16;0;-;-</lc>
					</c-comm>
					<c-c m="putClientProperty" s="2;O;O">
						<str id="3">vision.custom.functions</str>
						<o id="4" cls="java.util.HashMap"/>
					</c-c>
					<c-c m="setButtonBG" s="1;clr"><clr>-328965</clr></c-c>
					<c-c m="setText" s="1;str"><str>Reset</str></c-c>
				</c>
			</o-c>
		</o>
		<o cls="com.inductiveautomation.factorypmi.application.binding.action.ActionAdapter">
			<o-c m="setEventSet" s="1;java.beans.EventSetDescriptor"><ref>1</ref></o-c>
			<o-c m="setJythonCode" s="1;str"><str>event.source.parent.getComponent(&apos;Numeric Text Field 1&apos;).myCustomProp = None
#event.source.parent.getComponent(&apos;Numeric Text Field 1&apos;).intValue = None

</str></o-c>
			<o-c m="setMethodDescriptor" s="1;java.beans.MethodDescriptor"><ref>0</ref></o-c>
			<o-c m="setScopeStyle" s="1;com.inductiveautomation.ignition.common.script.ScriptScopeStyle"><ref>2</ref></o-c>
			<o-c m="setTarget" s="1;java.awt.Component">
				<c id="12" cls="com.inductiveautomation.factorypmi.application.components.PMIButton">
					<c-comm>
						<p2df id="5">146.0;27.0</p2df>
						<r2dd>510.0;106.0;146.0;27.0</r2dd>
						<str>btnNoWork 3</str>
						<lc>510.0;106.0;16;0;0.08219178;0.44444445</lc>
					</c-comm>
					<c-c m="putClientProperty" s="2;O;O">
						<ref>3</ref>
						<ref>4</ref>
					</c-c>
					<c-c m="setButtonBG" s="1;clr"><clr>-328965</clr></c-c>
					<c-c m="setText" s="1;str"><str>This Breaks</str></c-c>
					<c-c m="setPath" s="1;str"><str id="6"></str></c-c>
				</c>
			</o-c>
		</o>
		<o cls="com.inductiveautomation.factorypmi.application.binding.action.ActionAdapter">
			<o-c m="setEventSet" s="1;java.beans.EventSetDescriptor"><ref>1</ref></o-c>
			<o-c m="setJythonCode" s="1;str"><str>event.source.parent.getComponent(&apos;Numeric Text Field 1&apos;).myCustomProp = 3
#event.source.parent.getComponent(&apos;Numeric Text Field 1&apos;).intValue = None

</str></o-c>
			<o-c m="setMethodDescriptor" s="1;java.beans.MethodDescriptor"><ref>0</ref></o-c>
			<o-c m="setScopeStyle" s="1;com.inductiveautomation.ignition.common.script.ScriptScopeStyle"><ref>2</ref></o-c>
			<o-c m="setTarget" s="1;java.awt.Component">
				<c id="13" cls="com.inductiveautomation.factorypmi.application.components.PMIButton">
					<c-comm>
						<ref>5</ref>
						<r2dd>342.0;109.0;146.0;27.0</r2dd>
						<str>btnNoWork 2</str>
						<lc>342.0;109.0;16;0;0.08219178;0.44444445</lc>
					</c-comm>
					<c-c m="putClientProperty" s="2;O;O">
						<ref>3</ref>
						<ref>4</ref>
					</c-c>
					<c-c m="setButtonBG" s="1;clr"><clr>-328965</clr></c-c>
					<c-c m="setText" s="1;str"><str>This This is also Fine</str></c-c>
					<c-c m="setPath" s="1;str"><ref>6</ref></c-c>
				</c>
			</o-c>
		</o>
		<o cls="com.inductiveautomation.factorypmi.application.binding.action.ActionAdapter">
			<o-c m="setEventSet" s="1;java.beans.EventSetDescriptor"><ref>1</ref></o-c>
			<o-c m="setJythonCode" s="1;str"><str>#event.source.parent.getComponent(&apos;Numeric Text Field 1&apos;).myCustomProp = 2
event.source.parent.getComponent(&apos;Numeric Text Field 1&apos;).intValue = 2

</str></o-c>
			<o-c m="setMethodDescriptor" s="1;java.beans.MethodDescriptor"><ref>0</ref></o-c>
			<o-c m="setScopeStyle" s="1;com.inductiveautomation.ignition.common.script.ScriptScopeStyle"><ref>2</ref></o-c>
			<o-c m="setTarget" s="1;java.awt.Component">
				<c id="14" cls="com.inductiveautomation.factorypmi.application.components.PMIButton">
					<c-comm>
						<ref>5</ref>
						<r2dd>343.0;64.0;146.0;27.0</r2dd>
						<str>btnNoWork 1</str>
						<lc>343.0;64.0;16;0;0.08219178;0.44444445</lc>
					</c-comm>
					<c-c m="putClientProperty" s="2;O;O">
						<ref>3</ref>
						<ref>4</ref>
					</c-c>
					<c-c m="setButtonBG" s="1;clr"><clr>-328965</clr></c-c>
					<c-c m="setText" s="1;str"><str>This Is Fine</str></c-c>
					<c-c m="setPath" s="1;str"><ref>6</ref></c-c>
				</c>
			</o-c>
		</o>
		<o cls="com.inductiveautomation.factorypmi.application.binding.action.ActionAdapter">
			<o-c m="setEventSet" s="1;java.beans.EventSetDescriptor"><ref>1</ref></o-c>
			<o-c m="setJythonCode" s="1;str"><str>#event.source.parent.getComponent(&apos;Numeric Text Field 1&apos;).myCustomProp = 2
event.source.parent.getComponent(&apos;Numeric Text Field 1&apos;).intValue = None

</str></o-c>
			<o-c m="setMethodDescriptor" s="1;java.beans.MethodDescriptor"><ref>0</ref></o-c>
			<o-c m="setScopeStyle" s="1;com.inductiveautomation.ignition.common.script.ScriptScopeStyle"><ref>2</ref></o-c>
			<o-c m="setTarget" s="1;java.awt.Component">
				<c id="15" cls="com.inductiveautomation.factorypmi.application.components.PMIButton">
					<c-comm>
						<ref>5</ref>
						<r2dd>509.0;66.0;146.0;27.0</r2dd>
						<str>btnNoWork</str>
						<lc>509.0;66.0;16;0;0.08219178;0.44444445</lc>
					</c-comm>
					<c-c m="putClientProperty" s="2;O;O">
						<ref>3</ref>
						<ref>4</ref>
					</c-c>
					<c-c m="setButtonBG" s="1;clr"><clr>-328965</clr></c-c>
					<c-c m="setText" s="1;str"><str>This Won&apos;t Work</str></c-c>
					<c-c m="setPath" s="1;str"><ref>6</ref></c-c>
				</c>
			</o-c>
		</o>
		<o cls="com.inductiveautomation.factorypmi.application.binding.action.ActionAdapter">
			<o-c m="setEventSet" s="1;java.beans.EventSetDescriptor">
				<o cls="java.beans.EventSetDescriptor">
					<o-ctor s="5;str;java.lang.Class;[java.beans.MethodDescriptor;java.lang.reflect.Method;java.lang.reflect.Method">
						<str>propertyChange</str>
						<class>java.beans.PropertyChangeListener</class>
						<array cls="java.beans.MethodDescriptor" len="1">
							<o id="7" cls="java.beans.MethodDescriptor">
								<o-ctor s="2;java.lang.reflect.Method;[java.beans.ParameterDescriptor">
									<method>java.beans.PropertyChangeListener|propertyChange|1;java.beans.PropertyChangeEvent</method>
									<null/>
								</o-ctor>
							</o>
						</array>
						<method>java.awt.Container|addPropertyChangeListener|1;java.beans.PropertyChangeListener</method>
						<method>java.awt.Component|removePropertyChangeListener|1;java.beans.PropertyChangeListener</method>
					</o-ctor>
				</o>
			</o-c>
			<o-c m="setInvokeLater" s="1;b"><true/></o-c>
			<o-c m="setJythonCode" s="1;str"><str>print(&apos;{}: {}-&gt;{}&apos;.format(event.propertyName, event.oldValue, event.newValue))

if event.propertyName == &apos;myCustomProp&apos;:
	if event.newValue is None:
		print(&apos;is none!&apos;)
	event.source.myCustomProp = event.newValue
	
	
if event.propertyName == &apos;intValue&apos;:
	event.source.intValue = event.newValue
	
	
event.source.parent.getComponent(&apos;Numeric Text Field&apos;).intValue = event.source.parent.getComponent(&apos;Numeric Text Field&apos;).intValue + 1 </str></o-c>
			<o-c m="setMethodDescriptor" s="1;java.beans.MethodDescriptor"><ref>7</ref></o-c>
			<o-c m="setScopeStyle" s="1;com.inductiveautomation.ignition.common.script.ScriptScopeStyle"><ref>2</ref></o-c>
			<o-c m="setTarget" s="1;java.awt.Component">
				<c id="16" cls="com.inductiveautomation.factorypmi.application.components.PMINumericTextField">
					<c-comm>
						<p2df id="10">64.0;22.0</p2df>
						<r2dd>128.0;77.0;64.0;22.0</r2dd>
						<str>Numeric Text Field 1</str>
						<lc>128.0;77.0;16;0;0.1875;0.54545456</lc>
					</c-comm>
					<c-c m="putClientProperty" s="2;O;O">
						<ref>3</ref>
						<ref>4</ref>
					</c-c>
					<c-c m="setDynamicProps" s="1;java.util.TreeMap">
						<o cls="java.util.TreeMap">
							<o-ctor s="1;java.util.Comparator"><null/></o-ctor>
							<o-c m="put" s="2;O;O">
								<str id="8">myCustomProp</str>
								<o cls="com.inductiveautomation.factorypmi.application.binding.DynamicPropertyDescriptor">
									<o-c m="setDisplayName" s="1;str"><ref>8</ref></o-c>
									<o-c m="setName" s="1;str"><ref>8</ref></o-c>
									<o-c m="setPropertyType" s="1;java.lang.Class"><class>F</class></o-c>
									<o-c m="setShortDescription" s="1;str"><ref>6</ref></o-c>
									<o-c m="setValue" s="1;O"><flt>0.0</flt></o-c>
								</o>
							</o-c>
						</o>
					</c-c>
				</c>
			</o-c>
		</o>
	</arraylist>
	<ref>9</ref>
	<c cls="com.inductiveautomation.factorypmi.application.components.PMILabel">
		<c-comm>
			<p2df>212.0;31.0</p2df>
			<r2dd>215.0;248.0;212.0;31.0</r2dd>
			<str>Label 4</str>
			<lc>215.0;248.0;16;0;0.056603774;0.38709676</lc>
		</c-comm>
		<c-c m="setBackground" s="1;clr"><clr>-328965</clr></c-c>
		<c-c m="setText" s="1;str"><str>Number Of PropertyChange Events</str></c-c>
	</c>
	<c cls="com.inductiveautomation.factorypmi.application.components.PMINumericTextField">
		<c-comm>
			<ref>10</ref>
			<r2dd>438.0;253.0;64.0;22.0</r2dd>
			<str>Numeric Text Field</str>
			<lc>438.0;253.0;16;0;0.1875;0.54545456</lc>
		</c-comm>
		<c-c m="putClientProperty" s="2;O;O">
			<ref>3</ref>
			<ref>4</ref>
		</c-c>
		<c-c m="setValue" s="1;O"><int>6</int></c-c>
	</c>
	<c cls="com.inductiveautomation.factorypmi.application.components.PMILabel">
		<c-comm>
			<p2df id="11">100.0;34.0</p2df>
			<r2dd>230.0;100.0;100.0;34.0</r2dd>
			<str>Label 3</str>
			<lc>230.0;100.0;16;0;0.12;0.3529412</lc>
		</c-comm>
		<c-c m="setBackground" s="1;clr"><clr>-328965</clr></c-c>
		<c-c m="setText" s="1;str"><str>Custom Property</str></c-c>
	</c>
	<c cls="com.inductiveautomation.factorypmi.application.components.PMILabel">
		<c-comm>
			<ref>11</ref>
			<r2dd>232.0;58.0;100.0;34.0</r2dd>
			<str>Label 2</str>
			<lc>232.0;58.0;16;0;0.12;0.3529412</lc>
		</c-comm>
		<c-c m="setBackground" s="1;clr"><clr>-328965</clr></c-c>
		<c-c m="setText" s="1;str"><str>Builtin Property</str></c-c>
	</c>
	<c cls="com.inductiveautomation.factorypmi.application.components.PMILabel">
		<c-comm>
			<ref>11</ref>
			<r2dd>533.0;19.0;100.0;34.0</r2dd>
			<str>Label 1</str>
			<lc>533.0;19.0;16;0;0.12;0.3529412</lc>
		</c-comm>
		<c-c m="setBackground" s="1;clr"><clr>-328965</clr></c-c>
		<c-c m="setText" s="1;str"><str>Set To None</str></c-c>
	</c>
	<c cls="com.inductiveautomation.factorypmi.application.components.PMILabel">
		<c-comm>
			<ref>11</ref>
			<r2dd>365.0;20.0;100.0;34.0</r2dd>
			<str>Label</str>
			<lc>365.0;20.0;16;0;0.12;0.3529412</lc>
		</c-comm>
		<c-c m="setBackground" s="1;clr"><clr>-328965</clr></c-c>
		<c-c m="setText" s="1;str"><str>Set To integer</str></c-c>
	</c>
	<ref>12</ref>
	<ref>13</ref>
	<ref>14</ref>
	<ref>15</ref>
	<ref>16</ref>
</objects>

That will entirely depend on what builtin property you're setting. Some Java types are 'primitive', and cannot be null, at the JVM level. Most are not, and will happily allow nulls (whether the invisible Java setter method allows the null through is another matter, but it won't be a type coercion exception exactly in that case).

It is a deliberate decision in Swing's property change handling to fire if either new or old value are null; there's no meaningful way to compare null with null, so if either are null a change event is sent:
https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/java/beans/PropertyChangeSupport.html#firePropertyChange(java.lang.String,java.lang.Object,java.lang.Object)

1 Like