Table view not updating according to bindings - Part Two - cocoa

Title borrowed from this question, of which this one is not a duplicate. See my answer there for what was wrong for that questioner; I'm the author of that answer, and my problem is not that one.
I have a table view with three columns, whose Value bindings are bound to three properties of the arrangedObjects of an array controller. The array controller's contentArray is bound to the visitationResults of my document object; the items in that array are instances of a model class (VisitationResult). I have also bound the array controller's selectionIndexes and sortDescriptors to properties of my document.
I am mutating my property through a couple of accessors:
- (void) addVisitationResult:(VisitationResult *)newVisitationResult {
[self insertObject:newVisitationResult inVisitationResultsAtIndex:[self countOfVisitationResults]];
NSLog(#"arrayController arrangedObjects: %#", [arrayController arrangedObjects]);
}
That NSLog statement runs, and confirms that the array controller is gathering and arranging my model objects. This means that I am going through my property and getting KVO notifications for my document (which proves that the earlier questioner's problem, that of bypassing the property, is not the problem I'm having).
I added NSLog statements in my model object class's accessor methods. One of them is being called—by the array controller, in order to sort the objects (that property is the sort key). The other two, which the array controller doesn't know about, never get called.
Thus, my table view remains blank.

I found the problem: It's because I had explicitly bound the selectionIndexes and sortDescriptors bindings of the table view.
This wasn't necessary, anyway: I just checked, and the documentation says:
selectionIndexes
Typically, selectionIndexes is bound automatically to the NSArrayController that the first NSTableColumn is bound to.
sortDescriptors
Typically this binding is created automatically, binding to the sort descriptors of the NSArrayController of the initially bound NSTableColumn.
It appears that not only is it not necessary, but binding either or both of these two will break the table view.

Related

How is NSTableView Content Sorted After Array Controller Add?

After adding a managed object in array controller class, the bound table view 'places' it into the correct date sorted order.
However, as the table view is building it's rows, the new object has been placed at the bottom of the array controller's content array using:
Edit: Solution: Don't use the array controller's content. The issue was in:
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
....
// originally
NSManagedObject *ci = [[self.arrayController content] objectAtIndex:row];
// should be: (note arrangedObjects replacing content)
NSManagedObject *ci = [self.arrayController.arrangedObjects objectAtIndex:row];
....
}
The image below shows an example of the placement. Console output from within -tableView: viewForTableColumn: row: at left (from original code above) and ordering in the table view on right. LHR is the new object (notice date order ascending).
Tried:
Saving the moc right after adding newObject
Calling Array Controller's arrangeObjects method
Attempting to manually setSortDescriptors
Binding the table view's selection index to the array controller (for grins)
This is also messing up the selectedRow even though the row with LHR is selected in the table view right after adding. The only way to correct the array controller's order is to manually sort the column.
How can the array controller's content be in sync with the table view? Even more, why might they not be in the same order?
The array controller "arranges" its content. It does this by calling its -arrangeObjects: method. The default implementation filters the contents using the array controller's filterPredicate and then sorts it using its sortDescriptors. A subclass could override that method to arrange the content differently.
The arranged contents can be accessed using the arrangedObjects property. This is what corresponds to rows in the table view. So, you should always use this if you're indexing by a table row. For example:
NSManagedObject *ci = [self.arrayController.arrangedObjects objectAtIndex:row];
The array controller's sortDescriptors may be set via bindings (e.g. if the table view's sortDescriptors binding is bound to the array controller). Depending on how you set up the table view bindings, this may be automatic. For NSCell-based table views, you typically bind the table columns value binding and don't bind the table views bindings. In that case, the table view automatically binds its content, selectionIndexes, and sortDescriptors bindings to the same controller as its columns. For view-based table views, you typically don't bind table columns and have to bind those table view bindings explicitly if you want them bound.
You can also set the array controller's sortDescriptors programmatically.
Try invoking rearrangeObjects on the NSArrayController and then doing the selection you need subsequent to that in a dispatch_async block. It's maddening, but some of the bindings are deferred to the next runloop turn, so you can't simply add an object, call rearrangeObjects, and select the new object in synchronous code. You have to use GCD to schedule things
See this question/answer for more discussion. It's is the exact same issue. Adding an object and trying to select it.
Enabling the Auto Rearrange Content option of the Array Controller made it for me.

NSArrayController: How to programmatically clear selection?

Very simple question that's driving me crazy: What's the proper way to clear the selection of an NSArrayController programmatically?
I'm designing a view with the following components:
NSArrayController *controller1: Bound to an array of objects
NSPopUpView view1: Content bound to controller1.arrangedObjects; Value bound to controller1.selection; "Inserts Null Placeholder" selected
NSArrayController *controller2: Bound to an array stored in controller1.selection
NSPopupView view2: Content bound to controller2.arrangedObjects; value bound to controller2.selection; "Inserts Null Placeholder" selected
Initially, view1's content is populated; controller1 and controller2 have nil selection values; and view1 and view2 display null placeholders. Selection of controller1 causes controller1's selection to change and view2's content to populate. All good.
I'd like to implement a Clear button that clears the selection of controller1, which, thanks to bindings, should also clear the selection of controller2 and reset view1 and view2 to the null placeholder. For the life of me, I can't figure out the proper code for this very simple function. Altering the selection of controller1 fails to update the value shown in view1. Worse, altering the controller1 selection programatically causes weird things to happen in controller2: further selection of values in view1 fails to have any effect on view2.
Things I've tried:
Calling the SetSelectedObjects method of controller1 with an [NSArray new].
Calling the SetSelectedObjects method of controller1 with null.
Calling the SetSelectedIndex method of controller1 with NSNotFound.
Calling the RemoveSelectedIndex method of controller1 with the SelectedIndex property of controller1.
Looking in the Cocoa NSArrayController documentation for any class method or suggestion for clearing the selection value. Nothing there - not even any mention of this being desirable, let alone how to accomplish it.
Any ideas? Thanks...
According to Apples Developer documentation this can be done using setSelectionIndexes:
To deselect all indexes, pass an empty index set.
Objective-C:
[arrayController setSelectionIndexes:[NSIndexSet indexSet]];
Swift:
arrayController.setSelectionIndexes( NSIndexSet() )
Try controller1.selectionIndex = NSIntegerMax; and see if that works. I did a simple test with a label bound to the array controller's selection, and when I set the selectionIndex to NSIntegerMax, the no selection placeholder was displayed in the label.
As it turns out, the NSArrayController page does include a single recommendation for clearing the selection:
setSelectionIndexes:
Sets the receiver’s selection indexes and returns a Boolean value that indicates whether the selection changed.
(BOOL) setSelectionIndexes: (NSIndexSet *) indexes
Discussion
Attempting to change the selection may cause a commitEditing message which fails, thus denying the selection change.
To select all the receiver’s objects, indexes should be an index set with indexes [0...count -1]. To deselect all indexes, pass an empty index set.
However, I had actually tried that, and it still left me with a problem.
To generalize:
Create two classes, A and B, where A contains a property "NSArray *b_list" that contains a list of instances of B's.
Create an application with a property "NSArray *a_list". Populate it with some instances of A, and populate the b_list of each A instance with some B instances.
Create a window with two array controllers, Controller_A (bound to a_list) and Controller_B (bound to Controller_A.selection.b_list).
Create two pop-up buttons in the window, Popup_A (bound to Controller_A.arrangedObjects) and Popup_B (bound to Controller_B.arrangedObjects).
Create a "Clear" button with some logic to clear the selection of Controller_A. ("Some logic" is either the method recommended in the Apple documentation, or any other method.)
Run the application. Select an entry in Popup_A. Notice that Popup_B populates with the instances of Controller_A.selection.b_list, as it should.
Now hit the Clear button.
ERROR: Note that while the content and selection of Popup_A correctly become null, the same doesn't happen in Popup_B: it presents a blank entry with a single selected (blank) item. Note also that the selection properties of Controller_B indicates that there is a selection, but its properties are weird: selectedIndex points to 0 instead of NSNotFound, and selectedIndexes includes a non-empty selection integer range.
This is clearly an error with the binding logic that can definitely cause some exceptions and logical errors. For example, if there's any sort of binding attached to B_controller.selection, clearing A_controller will raise an exception relating to the selection value in B_controller, since it indicates a selection but points to garbage.
My workaround is not to bind anything directly to the B_controller selection. Instead, access B_controller programmatically, and in view of the selection value of A_controller, like this:
// setting some property c to the value of b_controller:
if ((B_controller.selectedIndex == NSNotFound) || A_controller.selectedIndex == NSNotFound))
c = nil;
else
c = [B_controller objectAtIndex: [B_controller.selectedIndex]];
I'm also submitting a bug report to Apple with this info.
Answer to a very old question but still.
I just needed the same thing, and for me this worked:
[arrayController setAvoidsEmptySelection:NO];
NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:
NSMakeRange(NSNotFound, 0)];
[arrayController setSelectionIndexes:indexes];

NSTableView backed by NSArrayController: why does setContent: work while IB doesn't?

I am trying to implement pretty much the simplest case of binding a NSTableView to a NSArrayController, so that the NSTableView is backed by an NSArray.
Here is the setup:
I have an NSArrayController whose 'Content Array' is bound to an NSArray in my app delegate.
In 'Object Controller' of the NSArrayController, the class name is set to Model, the type of objects contained in the NSArray.
The 'Value' of the single column of the NSTableView is bound to key 'name' of 'arrangedObjects' of the Array Controller, which is the only field of the Model class.
In applicationDidFinishLaunching: of my app delegate, I initialise the NSArray, and insert some Model objects.
However, the rows corresponding to Model do not appear in the table unless I also do: [self.arrayController setContent: self.array].
Is there a way I can get this to work using bindings wired up in Interface Builder? I would have expected the fact that the NSArrayController's 'Content Array' is bound directly to the NSArray to mean that I wouldn't have to set the content programmatically. Knowing why would help me understand bindings better.
Your array controller is observing your App Delegate's "array" property. That means KVO notifications are sent only when the array object is set, not when objects are added to it. It sounds like you are using an NSMutableArray and adding objects to it, which explains why the Array Controller is not being notified of changes, because the underlying object is not changing.
The easy solution is to wrap your calls in a will/did change block like so:
[self willChangeValueForKey:#"array"];
[self.array addObject:[NSDictionary dictionaryWithObject:#"foo" forKey:#"name"]];
[self.array addObject:[NSDictionary dictionaryWithObject:#"bar" forKey:#"name"]];
[self didChangeValueForKey:#"array"];
This manually notifies observers that there has been a change to the "array" property.
Long answer: You're doing it wrong. The whole point of having an array controller is to shift the work of managing the array to the controller class itself, so it manages the underlying array, sends out the right notifications, maintains state, etc. without you having to sweat the implementation details. A better solution would be to unhook the content array binding and just add objects to the array controller directly like so:
[arrayController addObject:[NSDictionary dictionaryWithObject:#"foo" forKey:#"name"]];
[arrayController addObject:[NSDictionary dictionaryWithObject:#"bar" forKey:#"name"]];
This works because the array controller manages its own array internally.
The best solution is to use Core Data. NSArrayController is designed to be used with it. You also get a whole bunch of things for free, like persistentce, undo support, object relationsips, and the ability to add objects without writing code just by calling add: on the array controller directly from your UI controls.

addObject to NSArrayController

Thanks for the help.
Core Data project. I'm importing text from a text file and I want to display it in an NSTextView whose value binding I have bound to the arrayController's selection with model key path text. The array controller contains instances of my entity, which has a string attribute named text. I want to update the arrayController for the key value bound to the textView so it can be saved. No errors when building, but not working. How do I do this?
id newObject = [arrayController newObject];
[arrayController addObject:newObject forKey:#"text"];
[newObject release];
[arrayController addObject:newObject forKey:#"text"];
This is “not working” because an NSArrayController doesn't respond to such a message. An array controller controls an array, not a key-value mapping; it does not have keys you can add objects for.
That, in turn, is because “array” in Cocoa means an ordered consecutive list, not an associative array. Cocoa calls a key-value mapping/associative array a “dictionary”.
The model key path is exactly that: The key path into the model of the property you want to bind the text view to. You seem to already know this; I assume you entered text here because it's what you named the attribute in your model. Your binding is correct.
But this also means that “text” has nothing to do with the array controller. It is a property of the model entities, not the controller. You need to set that property of the model object—in this case, newObject—not in the controller.

Cocoa bindings problem; bound table columns don't show any data, no errors in console

I'm having trouble converting my Cocoa project from a manually-synched interface model to a bindings model so that I don't have to worry about interface glue code.
I followed the CocoaDevCentral Cocoa Bindings tutorial to make sure that I had covered all the bases, but things aren't working correctly. I have a master-detail interface, but I'm having trouble even getting the master portion of the interface to work correctly. No data is showing up in the master column, even though I've set up the bindings model similar to how it is shown in the tutorial. I've made sure all my controllers and objects have -(id)key and -(void)setKey:(id)key methods so that they're bindings-compliant, I've created a ControllerAlias object in my nib, connected it to my controller, created an NSArrayController that binds to one of the NSMutableArrays from the class that ControllerAlias connects to, made sure to set the type of objects that are contained within the array, and then I've bound a table column to the NSArrayController.
I'm getting no errors whatsoever in the Console, and setting NSBindingDebugLogLevel to 1 doesn't produce any errors either, that would help me figure out what the problem is.
The only other thing I could think of to make sure that things are working correctly is to check that the NSMutableArray that connects to the NSArrayController actually has something in it, and it does.
Any suggestions? What other typical pitfalls are there with Cocoa bindings that I should check?
Have you put a breakpoint in your key: method to determine if it is getting called or not? If it isn't, then that would indicate that something isn't set up correctly for the binding in the table column (since you have verified that your array does have items in it).
I don't think that you need to create an Object Controller anymore (that tutorial is a bit out of date). Just create an Object in your NIB, and set its class to your Controller class. You can set up the bindings directly through it instead of the ObjectController.
To set up a binding, I do the following:
Create an instance of my controller in the NIB.
Create an NSArrayController, bind it to an array in my controller.
For each column in the table, bind the value to a member of an object in the array controller.
That should be all that you need to do - I think they've cleaned this up quite a bit since bindings were first introduced a few versions ago.
I've created a ControllerAlias object in my nib,
What is a “controller alias”? Is this a model, controller, or view?
connected it to my controller,
What do you mean?
created an NSArrayController that binds to one of the NSMutableArrays from the class that ControllerAlias connects to,
Classes don't have NSMutableArrays.
What property of the array controller did you bind?
What object did you bind it to?
What key path of that object did you bind it to?
… and then I've bound a table column to the NSArrayController.
What property of the table column did you bind?
Which property (key path) of the array controller did you bind it to?
So in my original code, I was modifying the array (which the NSArrayController was representing) in awakeFromNib, not in init, so the changes weren't being reflected in the interface since I wasn't modifying the array via a key-value observing method.
I changed the code from
theArray = [[NSMutableArray alloc] init];
[theArray addObject:newThing];
to:
theArray = [[NSMutableArray alloc] init];
NSMutableArray *bindingsCompliantArray = [self mutableArrayValueForKey:#"things"];
[bindingsCompliantArray addObject:newThing];
I think the other solution is to do the loading in the -(id)init method instead of the -(void)awakeFromNib method, but that required a larger refactor, so I didn't do that.
I figured this out by adding a button to create a new thing in the array list via the NSArrayController, and when I clicked the button, a new thing was added to the array and my existing array magically showed up as well.

Resources