1
votes

Ok, so vote it down if you have to but I'm about ready to through something as this is making me crazy. I have a core data application (OS X), with a couple NSTableViews (Cell Based) connected via NSArrayControllers. I have the Entity class' setup with some custom methods. I can add, remove, edit and do all kinds of stuff with the data - all is working great.

I decided to add a new column for a running sum and use the @sum as I have seen used. No matter what I do, I keep getting the error.

I have an Entity "Store" and another Entity "Item", they have a to-many relationship. In the Item entity I have a name and price attribute.

On the main window, I have two NSTableView's controller by NSArrayControllers, one for the Store and one for the Item. The content of the Item NSArrayController is controlled by the Store Controller - the selected item.

I added a new column to the Item NSTableView, bound it to the Item Controller and set it's model key path to @sum.price - this causes an error.

I am probably missing something simple, any thoughts on how to do this correctly?

Thank you.

--[EDIT]----

Store NSArrayController: - Object Controller - Entity Name: Store

  • ManagedObjectContext
    • Bound to Main Controller's ManagedObjectContext

Item NSArrayController:

  • Object Controller

    • Entity Name: Item
  • Content Set:

    • Bound to Store Array Controller

    • Controller Key: Selection

    • Model Key Path:item

Item NSTableView:

First Column:

  • Bound to Item Array Controller

  • Controller Key: arrangedObjects

  • Model Key Path: name

Second Column:

  • Similar, Model Key Path: price

The new sum column:

  • Bound to Item Array Controller

  • Controller Key: arrangedObjects

  • Model Key Path: @sum.price

The error I receive is: "the entity Item is not key value coding-compliant for the key "@sum"."

1
You need to be clear and specific about the bindings. For example, "The NSArrayController Item Controller has its Content binding bound to the NSArrayController Store Controller, controller key selection, model key path items. The new column's Content binding is bound to Item Controller, controller key ???, model key path @sum.price." Or whatever is actually the case. Which bindings of which objects are bound to what, with what controller key and what model key path? Also, you have left information out of the error, such as which class is not KVC-compliant.Ken Thomases
@KenThomases Thanks, please see edit.littleDrummerBoy

1 Answers

1
votes

Consider your first column. It is bound to Item Controller, arrangedObjects, name. Does each cell get an array of names? No. Each gets a single name.

Although that column binding is sometimes expressed as a key path like Item Controller.arrangedObjects.name, the way it actually works is that the column as a whole shows the arrangedObjects, one element per row, but name is applied to each element of that set separately. So, each cell has a single name.

Now consider your new column. The rows again correspond to the arrangedObjects of the Item Controller, but the model key path is applied to each element individually. But the model key path contains a collection operator, @sum, which isn't appropriate for an individual element (Item entity). Hence the error.

You could create a text field (outside of the table) which shows the sum of the price of all of the items of the selected store. You would bind the text field's Value binding to Item Controller, arrangedObjects, @sum.price. The text field works differently than the table column since it has a single thing to display. It really does use the result of [ItemController valueForKeyPath:@"[email protected]"]. The collection operator will be applied to a collection.

You could also bind a text field to Item Controller, selection, @sum.price to have it show the sum of the prices of the items selected in the item table.

Bindings don't provide any way to get a running sum, if I understand what you mean by that (first row shows the price of the first item, second row shows the sum of the prices of the first and second items, etc.). Such a running sum would be context dependent. A given row's value would depend on the previous rows' values. For example, sorting the table differently would mean that the running sum next to a given item would change, because the set of items before it had changed. Bindings can't do that. They don't know about position, index, or siblings.


Update:

To get a running sum, you'll need to not use bindings for the column. Make your view or window controller adopt NSTableViewDataSource if it doesn't already. Then connect the dataSource outlet of the table view to it.

In your data source class, implement -tableView:objectValueForTableColumn:row:. Check the column identifier. For any columns other than the running sum column, return nil so it uses the value from the column bindings.

For the running sum column, the straightforward-but-inefficient implementation would be something like:

NSRange range = NSMakeRange(0, rowIndex + 1);
NSArray* rowsToSum = [self.itemController.arrangedObjects subarrayWithRange:range];
return [rowsToSum valueForKeyPath:@"@sum.price"];

You would also need a way to inform the table view when cells in the running sum column need to be reloaded (recomputed). You would use Key-Value Observing to observe self for a change in the key path @"itemController.arrangedObjects.price". You would set this up in -viewDidLoad or -windowDidLoad. Don't forget to tear it down when the controller is done.

When the change notification is delivered — i.e. when -observeValueForKeyPath:ofObject:change:context: is called — you would call -reloadDataForRowIndexes:columnIndexes: on the table view to indicate that all row indexes in the running sum column should be reloaded.

That should work but it will be horribly inefficient once you get a significant number of rows.

So, to optimize, you should cache the running sums, but you need to be careful to invalidate the cache appropriately.

Basically, have an instance variable like _cacheIsValid. Like all instance variables, it will start out zero (false) by default. In -tableView:objectValueForTableColumn:row:, you'd check if it's valid. If it's not, you would build it and record that it's valid. Then, or if it was already valid, just return the element for the requested row.

To build the cache, iterate over self.itemController.arrangedObjects computing the running sum as you go and adding each value onto the end of an array. You could use a C-style array of primitive types or an NSMutableArray of NSNumbers, as you prefer. (The memory management for C-style arrays can be made simpler by using an NSMutableData for the buffer.)

You would invalidate the cache in -observeValueForKeyPath:..., before telling the table view to reload the running sum column.

For the next step in efficiency, you might recompute the cache at that time and compare the values to the existing cache (if it's valid) as you go. Accumulate in an NSMutableIndexSet the row indexes of only those rows for which the cached running sum actually changed and use that in the call to -reloadDataForRowIndexes:columnIndexes:. That way, the table view only reloads the cells that actually changed.