No validation with a bound NSTableView and custom NSFormatter

  • Hi all,

    I have an NSTableView whose data source is bound to an NSArrayController.  To one of its columns (displaying a custom type) I have attached a custom NSFormatter subclass.  The NSFormatter correctly returns NO from getObjectValue:forString:error: if the supplied string is not acceptable.  Despite this, however, validation is not enforced; when tabbing out of the field, the table view carries on and just assigns a null value to the corresponding property.

    Even when I implement control:didFailToFormatString:errorDescription: in the NSTableView's delegate, an invalid null value is nevertheless accepted and focus moves to the next responder, despite returning NO.

    My current implementation has been using an in-memory (transient) CoreData stack, but I have pared that back to simply use NSArrayController in "class" mode and behaviour is the same.

    The modelled property in question is a custom class (not a plist-compliant primitive, hence in part the need for a custom NSFormatter); however, two-way behaviour in the NSTableView works fine except for the lack of validation.

    I must be missing something fundamentally obvious, but I can't figure out where to approach the fix.  Enlightenment would be appreciated.

    thanks,

    -ben

    --
    Ben Kennedy, chief magician
    Zygoat Creative Technical Services
    http://www.zygoat.ca
  • On May 1, 2012, at 14:23 , Ben Kennedy wrote:

    > I have an NSTableView whose data source is bound to an NSArrayController.  To one of its columns (displaying a custom type) I have attached a custom NSFormatter subclass.  The NSFormatter correctly returns NO from getObjectValue:forString:error: if the supplied string is not acceptable.  Despite this, however, validation is not enforced; when tabbing out of the field, the table view carries on and just assigns a null value to the corresponding property.

    Double-check that "Validates immediately" is checked for the binding. Counter-intuitively, this doesn't mean "immediately" in the sense of every keystroke, but "immediately" in the sense of when editing ends. If it's not checked, validation is presumably deferred until an entire view's worth of fields has been entered.
  • On May 1, 2012, at 2:47 PM, Quincey Morris wrote:

    > On May 1, 2012, at 14:23 , Ben Kennedy wrote:
    >
    >> I have an NSTableView whose data source is bound to an NSArrayController.  To one of its columns (displaying a custom type) I have attached a custom NSFormatter subclass.  The NSFormatter correctly returns NO from getObjectValue:forString:error: if the supplied string is not acceptable.  Despite this, however, validation is not enforced; when tabbing out of the field, the table view carries on and just assigns a null value to the corresponding property.
    >
    > Double-check that "Validates immediately" is checked for the binding. Counter-intuitively, this doesn't mean "immediately" in the sense of every keystroke, but "immediately" in the sense of when editing ends. If it's not checked, validation is presumably deferred until an entire view's worth of fields has been entered.

    That seems to be at odds with the Bindings documentation: https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/Coc
    oaBindings/Concepts/MessageFlow.html


    The validation should be triggered as soon as the text field tries to commit editing.

    --Kyle Sluder
  • On May 1, 2012, at 14:55 , Kyle Sluder wrote:

    > That seems to be at odds with the Bindings documentation: https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/Coc
    oaBindings/Concepts/MessageFlow.html

    >
    > The validation should be triggered as soon as the text field tries to commit editing.

    That's what I meant by "when editing ends". The point was that validation doesn't occur keystroke by keystroke when "Validates immediately" is checked, even though it would be a reasonable guess from the title of the checkbox.

    If "Validates immediately" is *not* checked, no automatic verification occurs at any time. In that case, I assume, you're expected to trigger validation of all relevant fields manually at some point. (In the flow chart you referred to, there's no mention of validation except at Step 3.)
  • On 01 May 2012, at 2:47 pm, Quincey Morris wrote:

    > Double-check that "Validates immediately" is checked for the binding.

    Thanks for the reply Quincey.

    I confess to have mis-described my situation somewhat: while most columns were bound, several instead used the classical NSTableViewDataSource style, including the one for which I'm having the validation trouble.  Sure enough, when I connect a binding for that column, the formatter-imposed validation now works -- regardless of whether "validates immediately" is in force or not, contrary to your suggestion.  (I was certain that I had tested that already, but apparently I didn't.)

    However, the reasons I'm serving data for this column programatically are

    a) to offset the displayed value according to a document-level property (when the data set is in a "read-only" mode), and

    b) to be able to calculate and apply a related change to other columns when the current value is modified.

    Before I started using NSArrayController or bindings, validation used to work.  There must be something else I have overlooked here.  Why is validation failing for the non-binding (NSTableViewDataSource-backed) columns?

    Additionally: for a bound column, my understanding is that validate<Key>:error: should be called against the object to check validity (at least in the absence of an attached formatter).  However, it doesn't.  Is there a prerequisite I am failing to satisfy?

    thanks,

    -ben

    --
    Ben Kennedy, chief magician
    Zygoat Creative Technical Services
    http://www.zygoat.ca
  • On May 1, 2012, at 16:23 , Ben Kennedy wrote:

    > I confess to have mis-described my situation somewhat: while most columns were bound, several instead used the classical NSTableViewDataSource style, including the one for which I'm having the validation trouble.  Sure enough, when I connect a binding for that column, the formatter-imposed validation now works -- regardless of whether "validates immediately" is in force or not, contrary to your suggestion.  (I was certain that I had tested that already, but apparently I didn't.)
    >
    > However, the reasons I'm serving data for this column programatically are
    >
    > a) to offset the displayed value according to a document-level property (when the data set is in a "read-only" mode), and
    >
    > b) to be able to calculate and apply a related change to other columns when the current value is modified.
    >
    > Before I started using NSArrayController or bindings, validation used to work.  There must be something else I have overlooked here.  Why is validation failing for the non-binding (NSTableViewDataSource-backed) columns?
    >
    > Additionally: for a bound column, my understanding is that validate<Key>:error: should be called against the object to check validity (at least in the absence of an attached formatter).  However, it doesn't.  Is there a prerequisite I am failing to satisfy?

    I'm not aware that KVC-style validation ever happens automatically *except* for bindings that have have "Validates immediately" checked. This idea is supported by the KVC documentation:

    https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/Key
    ValueCoding/Articles/Validation.html#//apple_ref/doc/uid/20002173-CJBDBHCB


    AFAIK, the validation performed by a number formatter is limited to its own internal understanding (modified by the properties you set) of what a valid number looks like. I don't see how it could know what validate<Key>: method to call.

    OTOH, if your bindings-based validation isn't working at all, then there's also some other factor at play here. It's worth triple-checking that you haven't misspelled a property name or a method name, causing the validation method to no longer match the property.

    The other factor with validation methods is that the presence of a formatter changes what class of object the validation receives. Without any formatter, it gets a NSString (from a text field UI element). With a number formatter, it gets a NSNumber. Depending on how the validation method is coded, it might be doing the wrong thing if it gets an object of the "wrong" class.
  • On 01 May 2012, at 5:25 pm, Quincey Morris wrote:

    > I'm not aware that KVC-style validation ever happens automatically *except* for bindings that have have "Validates immediately" checked.

    Thanks for clarifying that -- I believe you're right.

    > AFAIK, the validation performed by a number formatter is limited to its own internal understanding (modified by the properties you set) of what a valid number looks like. I don't see how it could know what validate<Key>: method to call.

    What I meant was that since the formatter's statement of invalidity seemed to be ignored by the table view, I was then expecting the NSTableView machinery to at least call validate<Key>: on its data source -- and indeed it does, if both a binding is configured and "validates immediately" is checked.

    (Of course in retrospect this only makes sense for bindings, since in a NSTableViewDataSource setup there is no knowledge of the data model except within the data source's tableView:objectValueForTableColumn:... and tableView:setObjectValue:... methods.)

    The issue I'm still having is that non-bound columns (i.e., those controlled by the latter two NSTableViewDataSource methods) are not enforcing validity when the attached formatter returns NO from getObjectValue:forString:errorDescription:.

    cheers,

    b

    --
    Ben Kennedy, chief magician
    Zygoat Creative Technical Services
    http://www.zygoat.ca
  • On May 1, 2012, at 19:20 , Ben Kennedy wrote:

    > The issue I'm still having is that non-bound columns (i.e., those controlled by the latter two NSTableViewDataSource methods) are not enforcing validity when the attached formatter returns NO from getObjectValue:forString:errorDescription:.

    I don't know that I've ever had to deal with this specific scenario, but examination of the documentation leads to the following observations:

    1. You can implement a table view delegate that conforms to the NSTableViewDelegate protocol.

    2. That's a sub-protocol of the NSControlTextEditingDelegate protocol.

    3. That protocol has a method 'control:isValidObject:'.

    4. The documentation of that method:


    says this:

    > "This method gives the delegate the opportunity to validate the contents of the control’s cell (or selected cell). In validating, the delegate should check the value in the object parameter and determine if it falls within a permissible range, has required attributes, accords with a given context, and so on. Examples of objects subject to such evaluations are an NSDate object that should not represent a future date or a monetary amount (represented by an NSNumber object) that exceeds a predetermined limit."

    That seems like what you want, if you query the formatter directly from this delegate method.

    Whether NSTableView has an automated mechanism for querying the formatter itself, I don't know, and I can't find any documentation that suggests it might.

    I bet there's an easier answer staring us right in the face.
  • On 01 May 2012, at 7:42 pm, Quincey Morris wrote:

    > 3. That protocol has a method 'control:isValidObject:'.
    >
    >> "This method gives the delegate the opportunity to validate the contents of the control’s cell (or selected cell).
    >
    > That seems like what you want, if you query the formatter directly from this delegate method.

    I hadn't noticed that one before.  However, for some reason, it does not get called on my delegate.

    > Whether NSTableView has an automated mechanism for querying the formatter itself, I don't know, and I can't find any documentation that suggests it might.

    Well, the NSControlTextEditingDelegate protocol also provides 'control:didFailToFormatString:errorDescription:', which seems to be to the point of what I want:

    > Invoked when the formatter for the cell belonging to the specified control cannot convert a string to an underlying object.
    >
    > Return Value: YES if the value in the string parameter should be accepted as is; otherwise, NO if the value in the parameter should be rejected.

    And unlike control:isValidObject:, it does get called on my delegate.  Unfortunately, my return value of NO seems to be ignored.

    The thing about all this is that a few revs back, when I was using a dumb array with NSTableViewDataSource and no bindings, I was getting the validation behaviour for free.  Now it's gone.  I guess I should re-trace my steps and try to figure out what subtle critical difference I've introduced.

    cheers,

    b

    --
    Ben Kennedy, chief magician
    Zygoat Creative Technical Services
    http://www.zygoat.ca
  • On May 1, 2012, at 20:09 , Ben Kennedy wrote:

    > The thing about all this is that a few revs back, when I was using a dumb array with NSTableViewDataSource and no bindings, I was getting the validation behaviour for free.  Now it's gone.  I guess I should re-trace my steps and try to figure out what subtle critical difference I've introduced.

    When you bind one or more table columns to an array controller, the table view automatically binds its own content binding to the array controller (presumably with some heuristic for choosing one out of several, if you happened to bind different columns different ways). It doesn't seem impossible that the table view honors cell formatter errors only when its content binding isn't used (i.e. pure data source for all columns). That would explain the earlier behavior.
  • On 01 May 2012, at 9:09 pm, Quincey Morris wrote:

    > When you bind one or more table columns to an array controller, the table view automatically binds its own content binding to the array controller (presumably with some heuristic for choosing one out of several, if you happened to bind different columns different ways). It doesn't seem impossible that the table view honors cell formatter errors only when its content binding isn't used (i.e. pure data source for all columns).

    That seems like a reasonable explanation (although I see no inherent reason why it must be the case, nor have I found it documented, alas.)

    I have re-worked things to take full advantage of bindings now, so as to get the more desirable automated validation handling.  However, I've now come up against another challenge.

    As I may have alluded in an earlier posting, I need to be able to perform a cascading update on the data set in response to a particular row being edited.  Previously, this was trivial to do with a for loop in the delegate's -[tableView:setObjectValue:forTableColumn:row:], but now requires some trickery.

    I've found myself overriding -[editColumn:row:withEvent:select:] and -[textShouldEndEditing:] in a NSTableView subclass in order to have a hook into performing an operation post-edit in relation to the modified row.  This feels filthy, but I yet haven't seen how better to do it.

    I considered that I could register as a KVO observer for each of my data objects (and then use [arrayController.arrangedObjects indexOfObject:object] as the basis for my cascading update), but that requires adding extra -[addObserver:...] code accompanying everywhere I have an [arrayController addObject:], which seems prone to oversight.  Is there a better way?

    Separate from the above, I also have a new performance problem.  The loop in which I do my cascading updates executes inordinately slowly; simply assigning new values for two properties takes about 0.45 seconds when iterating a data set of a mere 135 objects.  It used to be instantaneous before bindings came into the picture.  I suspect that all of the KVO overhead is to blame (a quick peek in Instruments supports this).  Is there something fundamental I'm missing to avoid this bottleneck?

    thanks,

    b

    --
    Ben Kennedy, chief magician
    Zygoat Creative Technical Services
    http://www.zygoat.ca
  • On May 3, 2012, at 6:11 PM, Ben Kennedy wrote:

    > I have re-worked things to take full advantage of bindings now, so as to get the more desirable automated validation handling.  However, I've now come up against another challenge.
    >
    > As I may have alluded in an earlier posting, I need to be able to perform a cascading update on the data set in response to a particular row being edited.  Previously, this was trivial to do with a for loop in the delegate's -[tableView:setObjectValue:forTableColumn:row:], but now requires some trickery.
    >
    > I've found myself overriding -[editColumn:row:withEvent:select:] and -[textShouldEndEditing:] in a NSTableView subclass in order to have a hook into performing an operation post-edit in relation to the modified row.  This feels filthy, but I yet haven't seen how better to do it.
    >
    > I considered that I could register as a KVO observer for each of my data objects (and then use [arrayController.arrangedObjects indexOfObject:object] as the basis for my cascading update), but that requires adding extra -[addObserver:...] code accompanying everywhere I have an [arrayController addObject:], which seems prone to oversight.  Is there a better way?
    >
    > Separate from the above, I also have a new performance problem.  The loop in which I do my cascading updates executes inordinately slowly; simply assigning new values for two properties takes about 0.45 seconds when iterating a data set of a mere 135 objects.  It used to be instantaneous before bindings came into the picture.  I suspect that all of the KVO overhead is to blame (a quick peek in Instruments supports this).  Is there something fundamental I'm missing to avoid this bottleneck?

    I would say that to have this kind of performance issue generally means that bindings is not being worked with properly, and I would add that doing so is frequently an exceedingly frustrating thing. Following this thread it sounds to me that you would be served best by dumping bindings altogether and using the delegate model. It will give you far more control, will be more obvious and easier to debug, and will be more performant into tens to hundreds of thousands of rows. I know, because I have been in  a similar place, although I was experiencing slowdowns when getting into thousands of objects, rather than just 135.

    Don't get me wrong, I think bindings are great, and I use them extensively in almost everything I do. But it seems to me they are really built for straightforward usage and if you need to stretch them they tend to break. Not to mention that they are completely opaque and are a bear to debug.

    HTH,

    Keary Suska
    Esoteritech, Inc.
    "Demystifying technology for your home or business"
  • > On May 3, 2012, at 6:11 PM, Ben Kennedy wrote:
    >
    >> Separate from the above, I also have a new performance problem.  The loop in which I do my cascading updates executes inordinately slowly; simply assigning new values for two properties takes about 0.45 seconds when iterating a data set of a mere 135 objects.  It used to be instantaneous before bindings came into the picture.  I suspect that all of the KVO overhead is to blame (a quick peek in Instruments supports this).

    In defence of KVO, I must rescind this hasty conclusion.  The real problem occurred to me in the shower this morning: the array controller was constantly re-sorting the data after every property change, by virtue of its sortDescriptors.  By temporarily clearing the sortDescriptors before the loop (and restoring afterwards), performance is golden again.  I should have realized this earlier.

    On 03 May 2012, at 8:04 pm, Keary Suska wrote:

    > Don't get me wrong, I think bindings are great, and I use them extensively in almost everything I do. But it seems to me they are really built for straightforward usage and if you need to stretch them they tend to break. Not to mention that they are completely opaque and are a bear to debug.

    Having not used bindings much (despite their availability for many years), I appreciate your experience and perspective, because it's in line with the impressions I've nonetheless been forming.  (Cocoa bindings sort of remind me of BOOPSI from the Amiga days, akin to a well-intentioned but awkward and gangly teenager.)

    The motivation for moving my delegate-based table to a bound data source was in part to simplify the code needed for sorting and filtering in the UI -- but I was a bit naïve about the additional work implied by some of the trade-offs.

    thanks,

    b

    --
    Ben Kennedy, chief magician
    Zygoat Creative Technical Services
    http://www.zygoat.ca
previous month may 2012 next month
MTWTFSS
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      
Go to today