Bindings and object-pointer keys

  • Hello list,

    I'd like to apologize in advance for the length of this email; I would
    greatly appreciate any help anyone can give me.

    I've built a test case for the problem I'm having, available here
    (should be both Tiger and Leopard friendly):

    http://beta.darknoon.com/seth/TestBindings.zip

    If you were unwilling/unable to download the project, the setup is as
    follows. There is an array of 'Person' objects, each of whom has a
    name and a job. Each Job is uniquely defined by a title (i.e. two Jobs
    are considered equal if their title strings are equal) and has a
    salary. Every person who shares a job has a pointer to exactly the
    same job object. In other words, if we pretend -isEqualToJob is
    defined, "[[person1 job] isEqualToJob:[person2 job]]" would be
    logically equivalent to "[person1 job] == [person2 job]". Because of
    the shared nature of job objects, retitling all Secretaries as Office
    Assistants requires but one -setTitle: message, and changing salaries
    is just as easy.

    In the main interface, there are two table views, both controlled by
    instances of NSArrayController. One shows employees, the other
    displays jobs. The employees table view has three columns, name (bound
    to arrangedObjects.name), title (arrangedObjects.job.title), and
    salary (arrangedObjects.job). The salary column uses a custom
    'SalaryCell' which extracts a job's salary from a Job instance (hence
    the unusual binding). In some "future version," this will be necessary
    so the SalaryCell can draw itself differently to indicate different
    jobs.

    Phew, time to discuss the actual problem. Simply put, changes made to
    salaries in the job table are not reflected in the employees table
    without a forced refresh. To see this, simply create ('Hire') a
    person, use the pop up menu to set their job, and then try to change
    their salary. The salary column won't update until the next time the
    employees table becomes key.

    The reason for this problem is also rather simple -- technically, the
    person's job property (to which the 'salary' table column is bound)
    doesn't change. It's still a pointer to the exact same memory
    location. However, the object at the other end of that pointer has
    changed. Is there some way to make the bindings mechanism aware of
    that fact? The options I see are a) somehow track that changes have
    been made to a job object and then programmatically force the table to
    refresh, or b) just throw up my hands and switch to using Core Data.

    Is there a more elegant solution I'm missing? Failing that, which of
    the above options would you recommend?

    Thank you for taking the time to read this email,

    Seth Pellegrino
  • >
    > There is an array of 'Person' objects, each of whom has a name and a
    > job. Each Job is uniquely defined by a title (i.e. two Jobs are
    > considered equal if their title strings are equal) and has a salary.
    > Every person who shares a job has a pointer to exactly the same job
    > object.

      This is a bad idea for several reasons. I'll touch on the most
    relevant: This is poor object-oriented design. There should be a
    separate Job class. You don't need to compare equality if you can do
    something like this:

    (personOne.job == personTwo.job)

      Your approach is needlessly complicated, difficult to maintain, and
    prone to huge problems if your project gains complexity. Separate and
    encapsulate things properly from the beginning and you'll have far
    fewer headaches.

      Also, in business, a salary is typically not a function of a job
    that can have more than one position within a company. What if you
    have two "Computer Technicians" and one has been with the company for
    a few years while the other just started? If you're not giving your
    employees slight raises each year, they're going to quit. How do you
    maintain separate scales? Salary is typically assigned to the employee
    (your Person instances).

      In general, your model seems as though it could benefit from Core
    Data. I strongly recommend checking it out. With proper design, Job
    has a to-many relationship to Person and Person has a to-one
    relationship to Job. Core Data takes care of this kind of thing
    automatically and everything beautifully fits together.

    > The salary column won't update until the next time the employees
    > table becomes key.
    >
    > The reason for this problem is also rather simple -- technically,
    > the person's job property (to which the 'salary' table column is
    > bound) doesn't change.

      You need to define a dependent key. Look up -
    setKeys:triggerChangeNotificationsForDependentKey: in the documentation.

      More generally, you need to spend some time with the documentation.
    Look up "Key Value Observing" and read all related material. If you
    don't understand this, best not to use Bindings or Core Data at all.
    They are prerequisite knowledge.

    --
    I.S.
  • Mr. I. Savant,

    Thank you for your speedy reply -- I wasn't expecting anyone to be
    quite so quick to get back to me. I do have a few questions/
    clarifications for you below, if you're willing to indulge me.

    > Also, in business, a salary is typically not a function of a job
    > that can have more than one position within a company. What if you
    > have two "Computer Technicians" and one has been with the company
    > for a few years while the other just started? If you're not giving
    > your employees slight raises each year, they're going to quit. How
    > do you maintain separate scales? Salary is typically assigned to the
    > employee (your Person instances).

    I realize that salaries are typically assigned to people rather than
    jobs. This application is a contrived example meant to showcase the
    specific difficulty I was having with bindings. I apologize for not
    making that clearer in my original email.

    > You need to define a dependent key. Look up -
    > setKeys:triggerChangeNotificationsForDependentKey: in the
    > documentation.
    >
    > More generally, you need to spend some time with the documentation.
    > Look up "Key Value Observing" and read all related material. If you
    > don't understand this, best not to use Bindings or Core Data at all.
    > They are prerequisite knowledge.

    Dependent keys won't help me here. My problem is that a key in the
    Person class (namely the job key) is dependent on the keys of another
    class (namely the title and salary keys of the Job class). I've read
    the Key Value Coding/Observing documentation, and a fair bit of the
    Bindings documentation. As yet, I have found no simple way to register
    dependent key paths, which is what I need to be able to do.

    > This is a bad idea for several reasons. I'll touch on the most
    > relevant: This is poor object-oriented design. There should be a
    > separate Job class. You don't need to compare equality if you can do
    > something like this:
    >
    > (personOne.job == personTwo.job)
    >
    > Your approach is needlessly complicated, difficult to maintain, and
    > prone to huge problems if your project gains complexity. Separate
    > and encapsulate things properly from the beginning and you'll have
    > far fewer headaches.

    Could you elaborate on your meaning? There already is a Job class,
    each unique job (as defined by the title) is treated as a singleton
    (apologies if that's the wrong word to use).

    I realize this may not be the best solution, it's merely the best
    solution I've found so far. Conceptually, the problem I'm trying to
    solve is this: I have a set of data objects (in this example, People)
    with basically a "tag" (a Job). Tags have certain properties (like
    titles and salaries). Any of a tag's properties must be trivially
    changeable such that every tagged data object will reflect those
    changes.

    Another potential solution I came up with was to have each tag hold a
    mutable array of its associated data objects. Then changing tags for
    an object meant simply removing it from one array and adding it to
    another, and changing tag attributes only had to be done in one place.
    I decided against this approach, though, because I wanted to enable
    cross-tag ordering of data objects, and I could see no easy way of
    allowing that manipulation.

    > In general, your model seems as though it could benefit from Core
    > Data. I strongly recommend checking it out. With proper design, Job
    > has a to-many relationship to Person and Person has a to-one
    > relationship to Job. Core Data takes care of this kind of thing
    > automatically and everything beautifully fits together.

    I'm afraid my knowledge of Core Data is rather limited. It seems like
    it would work wonders for managing my data model, but I have a few
    reservations. Namely, none of the built-in persistent stores meet my
    needs. How difficult is it to either query the entire object graph (so
    I can write out a file manually) or provide a custom store type? On
    the other end, is there a simple way to purge the existing object
    graph so it can be read back in programmatically?

    More to the point, I'm not entirely sure Core Data would help me
    resolve my specific problem. Would changing a person's job's salary
    result in change notifications for the person's job? It seems like
    it's still the same difficulty with the bindings system, there's just
    a different model providing the data which is being bound to.

    I look forward to hearing back from you,

    Seth Pellegrino
  • On Dec 30, 2007, at 3:49 PM, Seth Pellegrino wrote:

    > Dependent keys won't help me here. My problem is that a key in the
    > Person class (namely the job key) is dependent on the keys of
    > another class (namely the title and salary keys of the Job class).
    > I've read the Key Value Coding/Observing documentation, and a fair
    > bit of the Bindings documentation. As yet, I have found no simple
    > way to register dependent key paths, which is what I need to be able
    > to do.
    >
    On Leopard: <http://developer.apple.com/DOCUMENTATION/Cocoa/Reference/Foundation/Protoco
    ls/NSKeyValueObserving_Protocol/Reference/Reference.html#//apple_ref/occ/cl
    m/NSObject/keyPathsForValuesAffectingValueForKey:
    >

    (Provided that the path doesn't include to-many relationships, which
    appears to be true in your case.)

    > I'm afraid my knowledge of Core Data is rather limited. It seems
    > like it would work wonders for managing my data model, but I have a
    > few reservations. Namely, none of the built-in persistent stores
    > meet my needs.
    >
    In what way are your needs not met?

    > How difficult is it to either query the entire object graph (so I
    > can write out a file manually)
    >
    It's not clear what you mean.
    You save changes by sending the managed objet context a save: message,
    and the file is saved.

    > or provide a custom store type?
    >
    <http://developer.apple.com/documentation/Cocoa/Conceptual/AtomicStore_Conce
    pts/Introduction/Introduction.html
    >

    > On the other end, is there a simple way to purge the existing object
    > graph so it can be read back in programmatically?
    >
    <http://developer.apple.com/documentation/Cocoa/Conceptual/CoreData/Articles
    /cdUsingMOs.html#//apple_ref/doc/uid/TP40001803-208900
    >

    Note, though, I wouldn't recommend using Core Data until you have more
    experience with Cocoa.

    mmalc
  • > >

    Thank you for the link, I wasn't aware the mechanism had changed. I'm
    now getting the following error:

    Person: A +keyPathsForValuesAffectingValueForKey: message returned a
    set that includes a key path that starts with the same key that was
    passed in, which is not valid. The property identified by the key path
    already depends on the property identified by the key, never vice versa.
    Passed-in key: job
    Returned key path set: {(
        "job.title",
        "job.salary"
    )}
    "n
    The "never vice versa" part of that error message doesn't leave me
    much hope. I tried setting up a key jobDependency which acted exactly
    like the job key, and then registered "job" as dependent on
    jobDependency.title and jobDependency.salary, but no dice. The
    application just silently fails, as before. Am I not using
    +keyPathsForValuesAffectingValueForKey: properly? Is there a
    workaround I'm missing?

    >> I'm afraid my knowledge of Core Data is rather limited. It seems
    >> like it would work wonders for managing my data model, but I have a
    >> few reservations. Namely, none of the built-in persistent stores
    >> meet my needs.
    >>
    > In what way are your needs not met?

    I need to write out my file in a format which isn't XML, SQLite, or a
    straight binary representation of my object graph.

    >> How difficult is it to either query the entire object graph (so I
    >> can write out a file manually)
    >>
    > It's not clear what you mean.
    > You save changes by sending the managed objet context a save:
    > message, and the file is saved.

    Right, but it is my understanding that this uses a persistent store to
    save the data. Basically, my question is how can an in-memory store be
    manually written out to disk? Is such a thing even possible?

    > >

    Thanks for the link.

    >> On the other end, is there a simple way to purge the existing
    >> object graph so it can be read back in programmatically?
    >>
    > <http://developer.apple.com/documentation/Cocoa/Conceptual/CoreData/Articles
    /cdUsingMOs.html#//apple_ref/doc/uid/TP40001803-208900
    > >
    >
    > Note, though, I wouldn't recommend using Core Data until you have
    > more experience with Cocoa.

    That is indeed a reason I'm hesitant to adopt Core Data. Another
    reason I'm hesitant to adopt core data is that it won't solve this
    particular problem. I put together a core data version of the
    EmployeeManager application, and the same problem is still present.

    Thanks for your reply,

    Seth Pellegrino
  • On Dec 30, 2007, at 8:23 PM, Seth Pellegrino wrote:

    >> >
    >
    > Thank you for the link, I wasn't aware the mechanism had changed.
    > I'm now getting the following error:
    > Person: A +keyPathsForValuesAffectingValueForKey: message returned a
    > set that includes a key path that starts with the same key that was
    > passed in, which is not valid. The property identified by the key
    > path already depends on the property identified by the key, never
    > vice versa.
    > Passed-in key: job
    > Returned key path set: {(
    > "job.title",
    > "job.salary"
    > )}
    > "n
    > The "never vice versa" part of that error message doesn't leave me
    > much hope. I tried setting up a key jobDependency which acted
    > exactly like the job key, and then registered "job" as dependent on
    > jobDependency.title and jobDependency.salary, but no dice. The
    > application just silently fails, as before. Am I not using
    > +keyPathsForValuesAffectingValueForKey: properly? Is there a
    > workaround I'm missing?
    >
    This means that one of the keys in the you're returning is the same as
    the key passed in.  An equivalent mistake would be:

    + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
    {
        NSSet *keyPaths = [super
    keyPathsForValuesAffectingValueForKey:key];

        if ([key isEqualToString:@"fullName"])
        {
            NSSet *affectingKeys = [NSSet setWithObjects:@"fullName",
    @"firstName", @"department.deptName", nil];
            keyPaths = [keyPaths setByAddingObjectsFromSet:affectingKeys];
        }
        return keyPaths;
    }

    (notice @"fullName" in the returned set for @"fullName").

    >>> I'm afraid my knowledge of Core Data is rather limited. It seems
    >>> like it would work wonders for managing my data model, but I have
    >>> a few reservations. Namely, none of the built-in persistent stores
    >>> meet my needs.
    >> In what way are your needs not met?
    > I need to write out my file in a format which isn't XML, SQLite, or
    > a straight binary representation of my object graph.
    >
    You can do that with a custom store type on Leopard, or using an in-
    memory store on Tiger.

    >>> How difficult is it to either query the entire object graph (so I
    >>> can write out a file manually)
    >> It's not clear what you mean.
    >> You save changes by sending the managed objet context a save:
    >> message, and the file is saved.
    > Right, but it is my understanding that this uses a persistent store
    > to save the data. Basically, my question is how can an in-memory
    > store be manually written out to disk? Is such a thing even possible?
    >
    You have to make sure you have all the objects and archive them
    yourself.  This is possible, but depending on your schema may be tricky.

    >> Note, though, I wouldn't recommend using Core Data until you have
    >> more experience with Cocoa.
    > That is indeed a reason I'm hesitant to adopt Core Data. Another
    > reason I'm hesitant to adopt core data is that it won't solve this
    > particular problem. I put together a core data version of the
    > EmployeeManager application, and the same problem is still present.
    >
    I suspect the problem is with your implementation per above.

    mmalc
  • On Dec 30, 2007, at 10:40 AM, Seth Pellegrino wrote:

    > In the main interface, there are two table views, both controlled
    > by instances of NSArrayController. One shows employees, the other
    > displays jobs. The employees table view has three columns, name
    > (bound to arrangedObjects.name), title (arrangedObjects.job.title),
    > and salary (arrangedObjects.job). The salary column uses a custom
    > 'SalaryCell' which extracts a job's salary from a Job instance
    > (hence the unusual binding). In some "future version," this will be
    > necessary so the SalaryCell can draw itself differently to indicate
    > different jobs.
    >
    > Phew, time to discuss the actual problem. Simply put, changes made
    > to salaries in the job table are not reflected in the employees
    > table without a forced refresh. To see this, simply create ('Hire')
    > a person, use the pop up menu to set their job, and then try to
    > change their salary. The salary column won't update until the next
    > time the employees table becomes key.
    >
    > The reason for this problem is also rather simple -- technically,
    > the person's job property (to which the 'salary' table column is
    > bound) doesn't change. It's still a pointer to the exact same
    > memory location. However, the object at the other end of that
    > pointer has changed. Is there some way to make the bindings
    > mechanism aware of that fact? The options I see are a) somehow
    > track that changes have been made to a job object and then
    > programmatically force the table to refresh, or b) just throw up my
    > hands and switch to using Core Data.
    >
    > Is there a more elegant solution I'm missing? Failing that, which
    > of the above options would you recommend?

    There may be a more elegant or correct solution, but the following
    should work:

    Create a "synthetic" property on the Job class, let's call it
    salaryData.  Use setKeys:triggerChangeNotificationsForDependentKey:
    to make the salaryData property dependent on the salary key (and any
    other keys/properties on which SalaryCell bases its display and
    behavior).  Then, bind your salary column to the salaryData property.

    So, what should the salaryData getter return?  As a first pass,
    perhaps just 'self' -- the Job instance, itself.  In the future,
    though, you might want to return some value object (e.g. a
    dictionary) which really represents the inputs for the SalaryCell.

    The theory here is that the SalaryCell doesn't really depend on the
    whole job nor its identity.  In all probability, in a real-world
    application, a Job will have many properties, some of them relevant
    to the appearance and behavior of a SalaryCell, but most not.  In
    that case, you really don't want the SalaryCell to update itself
    every time _any_ property of a Job is changed.  It should really be
    bound to the specific aspects of a Job which are really relevant.
    So, SalaryCell should not be bound to the job instance, but to the
    property(ies) which are really relevant.

    Put another way, I think both the existence of SalaryCell and your
    struggles to get bindings to do what you want are hints that you need
    another class.  There should be a value-object class, perhaps Salary,
    which aggregates the data relevant to representing salaries.  Your
    description of SalaryCell makes it clear that a simple number isn't
    sufficient; otherwise, you'd just bind SalaryCell to
    arrangedObject.job.salary.  So, whatever is the thing which a
    SalaryCell really represents, that's the contents of a Salary object.

    You might be thinking that a Salary object is what your Job object
    already was.  The difference is that a Job is an entity.  Its
    identity is significant, and multiple Person objects share Job
    objects.  A Salary is a value object.  So, maybe it will end up being
    the case that a Job is a thin shell around a Salary.  That's fine if
    that's what is needed.  That way, multiple Person objects may all
    refer to a single Job, but when the Job's Salary is changed, that's
    done in a proper KVO-conforming way (for example, using -[Job
    setSalary:]).

    I hope that helps.

    Cheers,
    Ken
  • On Dec 31, 2007, at 3:18 AM, Ken Thomases wrote:

    > There may be a more elegant or correct solution, but the following
    > should work:
    >
    > Create a "synthetic" property on the Job class, let's call it
    > salaryData.  Use setKeys:triggerChangeNotificationsForDependentKey:
    > to make the salaryData property dependent on the salary key (and any
    > other keys/properties on which SalaryCell bases its display and
    > behavior).  Then, bind your salary column to the salaryData property.
    > <snip>
    > I hope that helps.

    Indeed, that helped a great deal. I  do see your point about a salary
    value-object as well, and I'll definitely spend some time thinking
    about that path.

    Thank you again, sir, your solution works very well.

    Also, thanks are due to Mr. mmalc crawford, your information about
    Core Data is very helpful. However, I think I'm going to take your
    advice and spend some more time with Cocoa before I actually use Core
    Data.

    Seth Pellegrino
previous month december 2007 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