How To Avoid Triggering Property Setter Method

12 views (last 30 days)
We are using set methods in a number of our handle derived classes to do extended property validations.
The general requirement of what we did before was:
  1. Check that the new value is OK
  2. Backup the old value
  3. Set the new value
  4. Run a series of calculations
  5. If an error occurrs during calculations, restore the old value
Previously we were using dynamic properties for this which allowed for a completely custom set function to be used. We are however now trying to switch to non-dynamic properties due to the performance penalties of dynamic prop lookups.
In order to acheive similar behaviour, we need to use the set.PropName function for each of the non-dynamic properties. Ideally this set function would just call our original set method directory:
function set.PropName(obj, val)
obj.customSetter('PropName',val);
end
However this according to the help text won't work because if we set the property value in customSetter, then "property assignments made from functions called by a set method do call the set method" which would surely cause an infinite loop, and seems a rather strange design "feature".
Instead we end up with this kind of structure:
function set.PropName(obj, val)
[doc, val] = obj.setFuncPre('PropName', val);
oldVal = obj.PropName;
obj.PropName = val;
me = obj.setFuncPost('PropName', doc);
if ~isempty(me)
obj.PropName = oldVal;
obj.setFuncRestored('PropName', me);
end
end
Which becomes incredibly long winded when we have hundreds of properties across multiple classes which all have to do the same thing.
So the question is really, is there a way to bypass calling the setter function so that we can use the first example? Or is there a better approach to this?

Accepted Answer

Dave B
Dave B on 6 Nov 2020
Hi Thomas -
This is an interesting problem. If I understand correctly, you need to have the property set because you have some validation that is going to depend on the 'new' value being stored in the property (i.e. the post-set calculations that might throw an error).
I can think a few strategies, but none are perfect. In terms of 'is there a better approach' I suppose it would be ideal if you could validate the new values without needing to 'commit' the set, but I can easily see why that might be challenging for a really complex object/calculation.
I can't think of a way to prevent the setter from recursing when using a separate method - I tried some tricks like anonymous functions and evalin to no avail. Maybe you could use a logical 'lock' property to achieve the same effect? It's not a great solution but it would clean up the code a little bit (example below).
Some alternate strategies include making a public and a private version of each property to add another 'layer' (this probably adds just as much complexity) or to make use of PreSet and PostSet listeners (however this means storing the old value somewhere, so I think not great).
The following code keeps the line that does the actual setting in the setter. It prevents recursion by using the customSetter when called directly (obj.setlock is false) but a direct set when called from customSetter (obj.setlock is true). I think you can use one customSetter for multiple properties (you'd need to pass the property name as an argument), but whether or not you can achieve one 'lock' property depends on whether setting a property can have a side effect on another property.
Note that I used try/catch to trap the errors, but your me = ... line would work just as well here if that's how you track your exceptions. However it is critical is that the setlock property is set to false by the end of customSetter no matter what happens. Also note - I didn't handle the potential conflict around saving and loading objects in the class - I think that's okay because the object can't be saved with prop in an invalid state, so it doesn't matter which branch the setter goes down (I think it will always do the customSetter branch).
classdef foo_set < handle
properties
prop
end
properties (Transient, Access=private)
setlock=false;
end
methods
function set.prop(obj,val)
if ~obj.setlock
obj.customSetter(val);
else
obj.prop=val;
end
end
function customSetter(obj,val)
% 1. Do pre-validation here using val
oldval = obj.prop; % 2. Backup the old value
obj.setlock=true; % A. lock setter to prevent recursion
obj.prop=val; % 3. Set the new value
try
% 4. Run post-set validations here
catch
% 5. Error occurred, restore old value
obj.prop=oldval;
end
% B. Whether or not error occurred, unlock the setter
obj.setlock=false;
end
end
end
  1 Comment
Thomas Carpenter
Thomas Carpenter on 6 Nov 2020
Edited: Thomas Carpenter on 6 Nov 2020
Hi Dave,
Thanks for your response. You've pretty much hit the nail on the head with what we are trying to do - essentially the value has to be committed as the postSet step triggers a whole chain of calculating and validating other properties which need the new value locked in (to handle passing the new value through to each step without setting it would be rather difficult to manage as multiple properties can end up being set before it finally resolves itself).
We basically came to the same idea as you've presented in your example, having a flag to detect whether the value is being set or not. Although we used onCleanup to ensure the flag gets restored, but try-catch should work fine too.
By returning the value from our custom set function and doing the flag checking inside too, we can get the setter down to a one-liner which is much improved over what we had before. It ends up setting it twice as a result, but the second time will be the same value as the first so that doesn't really matter.
function set.NTimes(obj, val)
obj.NTimes = obj.validatedSetProperty('NTimes',val);
end
In our case the flag is a property called IsSettingPropVal.
function val = validatedSetProperty( obj, propName, val )
if obj.IsSettingPropVal
% If it was us that just set the property value, skip trying to set it.
return
end
%... pre-set stuff
oldVal = obj.(propName);
% Set the new value using our anti-infinite-loop set function
setVal(obj,propName,val);
try
%... post-set stuff
catch me
% Restore if we need to
val = oldVal;
setVal(obj,propName,val);
%... handle error message
end
end
function setVal(obj, propName, val)
% Ensure that no matter what happens, when this function returns, we
% clear the in setter function flag.
fin = onCleanup(@()restoreNotInSetter(obj));
% Mark that we are in a setter function
obj.IsSettingPropVal = true;
% Set the property value
obj.(propName) = val;
end
% Called by onCleanup in the setVal function to ensure that even if an
% error occurs while setting the property, we clear the IsInSetter flag.
function restoreNotInSetter(obj)
obj.IsSettingPropVal = false;
end

Sign in to comment.

More Answers (0)

Categories

Find more on Entering Commands in Help Center and File Exchange

Products


Release

R2019a

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!