How do I create a tableview with a variable number of columns where the extra columns have checkboxes in them?

I am building Mac OS application using Objective C and Cocoa.


I am somewhat familiar with bindings (enough to get in trouble).

I can build and display tables with bindings without a problem.


What I want to do is depending upon other factors, be able to add some number of columns to a tableview where the column cell content is a Check Box button.


In my TableViewControlller in the viewDidLoad method I am adding columns with names and adding the binding for the column there:

NSString *keyPath = [NSString stringWithFormat:@"arrangedObjects.field%ld", index];

[aCol bind:@"value" toObject:arrayController withKeyPath:keyPath options:nil];

(the fields in the array are labeled field1, field2, field3, ....)


The columns and their titles do show up when the table is displayed. The rows of data for the fixed columns in the table show up. So the bindings for the fixed columns are working.


- (NSView *)tableView:(NSTableView *)inTableView

viewForTableColumn:(NSTableColumn *)tableColumn

row:(NSInteger)row


gets called and there I add my check box NSTableCellView. But I can't do any binding there because the check box button has not been loaded yet.

I can see that NSTableCellVew has been fetched as a result of:

result = [inTableView makeViewWithIdentifier:@"whoCheckBox" owner:self];

I would expect that I would get access to the check box button with a [result objectValue] call, but objectValue is still zero here.


Now where/when is that button going to be actually available to bind it to the NSTableCellView?


I tried turning off bindings and adding columns manually and that seems to work.

The table displays with the original value of the check box cell that Cocoa copied from the xib.

BUT I haven't found a way to load the values from my data source into the check boxes.


- (id)tableView:(NSTableView *)inTableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row


is implemented, but the values I return for the checkbox columns are ignored.

I am returning: return [NSNumber numberWithBool:isChecked];

where isChecked is a bool and is either true or false. I have used an NSNumber to charnge the value in check boxes in columns that were created in Interface Builder, so that does not appear to be the issue.


Help!

1. For a view-based table view, you do not use column bindings:


developer.apple.com/library/content/documentation/Cocoa/Conceptual/TableView/PopulatingViewTablesWithBindings/PopulatingView-TablesWithBindings.html


All you need to do is bind the table view content to (in your case as in the above document) an array controllers' "arrangedObjects".


2. If you bind the table content this way, you do not implement "tableView:objectValueForTableColumn:row:" because the objectValue for each row is found via the binding.


This objectValue is set as the value of the "objectValue" property of the NSTableCellView for each row/column.


Note that there are 3 distinct mechanisms for getting the "objectValue" property of the NSTableCellView set correctly:


a. By binding the table view content. (In essence, the data source isn't used.)


b. By not binding the table view content, but implementing the "tableView:objectValueForTableColumn:row:" delegate method.


c. By not binding the table view content, but setting the cell's "objectValue" property manually inside the "tableView:viewForTableColumn:row:" delegate method, just after you've obtained the cell.


Obviously, (c) is the only one of the three that lets you know you the objectValue before the delegate method returns.


3. Since (you say) all your fixed columns are populated by bindings, the objectValue for each row is going to be the corresponding "arrangedObjects" element. In particular, the objectValue for each column in the row is the same object, and the value for each column is obtained from different properties of the objectValue object. (Read the above document carefully and you'll see that's what it suggests you do.)


You can have a different objectValue for each column, but not (AFAIK) when you use a table view content binding.


4. So, now you're ready to create the other binding, from the checkbox to the object that holds its value, inside "tableView:viewForTableColumn:row:". It doesn't matter that objectValue is still nil (in your case), because you're not binding to that, but to the table cell "result", using a keypath that begins with "objectValue". The question is: what is the rest of the keypath?


If your "field<X>" properties are boolean, the answer is easy. Use keypath "objectValue.field<X>".


If it's not, and you need to do a calculation to determine a "isChecked" value, then you have more work to do. The simplest approach is to provide an "isChecked<X>" derived property in your data model. This isn't hard, but you need to do two things:


a. Write a getter that computes the correct boolean value from the value of "field<X>".


b. Provide a class method of the form "keyPathsForValuesAffectingIsChecked<X>" that returns a set containing the key path that the property is derived from, namely "field<X>".


I'm not sure this addresses all the problems you've run into, but maybe it will get you a bit further along. FWIW, I think using content bindings in view-based table view is a waste of time and brain power. Doing it manually via the data source and delegate is usually easier and clearer.

Your reply was quite helpful and I quit trying to figure how to get bindings working with this. Too much behind the scenes magic wasting my time.

Once I knew I was looking for a solution avoiding bindings, I found a couple gems in the TableViewPlayground sample code that I am pretty sure are not explicitly mentioned in the documentation. Below are details of what I had to do to get a tableView operating without bindings, which IMO, is far easier than bindings once you are aware of the process.


VERY IMPORTANT KEYS to building View based DATASOURCE TableViews



1. USE BASIC UI items (NSButton), NOT NSTableCellView

2. Contrary to #1, you can use NSTableCellView for TextFields, imageViews IF... see below.

3. Add your basic UI Items directly to the NSTableColumn, delete provided the NSTableCellView.

4. Carefully implement tableView:inTableView viewForTableColumn:inTableColumn row:inRow.


Maybe (probably) I am blind, but I trusted the default mechanisms in Interface Builder regarding

building tables. Well, they are probably alright if your table contains nothing but text fields

and a static number of columns and you use bindings.


My implementation called for checkboxes and I also wanted to be able to change the number of

table columns at run time. Just having checkboxes requires knowing the following.


When Interface Builder creates a default tableView and you set the number of columns that you

want, it gives you a bunch of columns with NSTableCellView in each column.

If you then utilize bindings, this is possibly not a bad thing.


If you want to put a button into that column or use NSTableViewDataSource to supply your data

THIS IS A VERY BAD THING.

You must drag your Check Box Button (NSButton) to be the direct sub item of the column you want

it to be in. Then delete the NSTableViewCell - it is useless.



Make sure whatever class you are using to support your table is hooked up as both the dataSource

and the delegate for the TableView.



Then as the documentation says you need to implement:


NSTableViewDataSource METHODS



- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView

{

return rowCount; //fetch your row count from your data source.

}



// this is called to supply the data for every column for every row, so be efficient as you can.

- (id)tableView:(NSTableView *)inTableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row

NSString *colName = [tableColumn title]

{

id aRecord = [self objectAtIndex:row]; //fetching a record from my data source

if ([colName isEqualToString: @"Number"]) {

return [[aRecord field:eNumberField] stringValue]; //my record returns a NSNumber, text field requires text.

}

else if ([colName isEqualToString: @"Name"]) {

return [aRecord field:eNameField];

} else {

bool isChecked = [aRecord column:colName field:eButtonField];

return [NSNumber numberWithBool:isChecked];

}

}



but NEVER:

- (void)tableView:(NSTableView *)tableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row


You have to implement appropriate action methods for each of your column UI elements.

In your action method you can discover the row/column information with the following methods:

(tableView is assumed to be a (NSTableView*) field in the class that implements your action method that gets loaded

automagically because you tied the TableView to tableView in your class in Interface Builder)

- (IBAction)yourActionMethod:(id)sender

{

NSInteger theRow = [tableView rowForView:sender];

NSInteger theCol = [tableView columnForView:sender];

NSTableColumn *aColumn = [[tableView tableColumns] objectAtIndex:theCol];

NSString *colName = [aColumn title];

......

}



NSTableViewDelegate METHODS:


- (NSView *)tableView:(NSTableView *)inTableView

viewForTableColumn:(NSTableColumn *)tableColumn

row:(NSInteger)row



STUFF HERE IS REALLY IMPORTANT, without it nothing works.


1. The identifier of each table column UI element MUST be set and unique.

(text fields, buttons, ...)

2. You set it in Interface Builder when viewing the class for the UI element for the table column,

it is called "Identity". Whatever text you put here, you use to identify the column UI element in your code.

3. This "Identity" name is used in the makeViewWithIdentifier call below. This MUST be done.

4. IF you do have a text field and you DID leave the NSTableCellView in place, note

the code for "Number" and "Name" below. Assigning a value to the textField.stringValue must be done

here or else no values are ever displayed for this column ever.

(I suspect this is the case for the imageView field of a NSTableCellView as well)

5. Note that for my button for the third column I return the NSButton I put into my tableView

in Interface Builder. Even though I have a variable number of button columns in my table, I always

have one. For my table I have two text columns and everything else is a button.

6. Table Column titles are NOT the same as the UI element Identity, although maybe they could be.)

7. Note that I do NOT set the button's value in this code, for some reason Apple does not require it

like it does with the textField in a NSTableCellView.


8. I don't know how to create a column UI element "template" if there isn't one in a column in your TableView

that you created in Interface Builder. Maybe if you add your view in the objects list in IB???

Definitely does not work if your UI element is a separate view in your .xib file.


Here is some code (it originally came from Apple, just not their documentation):



- (NSView *)tableView:(NSTableView *)inTableView

viewForTableColumn:(NSTableColumn *)tableColumn

row:(NSInteger)row

{

NSTableCellView *result;

NSString *colName = [tableColumn title];

id aRecord = [self objectAtIndex:row]; //retrieve this row's record from my datasource.

if ([colName isEqualToString: @"Number"]) {

result = [inTableView makeViewWithIdentifier:@"Number" owner:self];

result.textField.stringValue = [ [aRecord field:eNumberField] stringValue];

return result;

}

else if ([colName isEqualToString: @"Name"]) {

result = [inTableView makeViewWithIdentifier:@"Name" owner:self];

result.textField.stringValue = [aRecord field:eNameField];

return result;

} else {

NSButton *theButton = [inTableView makeViewWithIdentifier:@"myCheckBox" owner:self];

return theButton;

}

}

A lot of what you said in that last post is kinda sorta correct, but not quite so because you haven't fully absorbed all of the documentation and behavior yet.


For example, NSTableCellView is a general-purpose container for the subviews of a cell in a table view. For convenience, it comes with "text" and "image" properties that can be connected to a text field and image subview, and has built-in behavior to lay these subviews out in a standard way. In a new table view created in IB, you get cells with just a text field subview (no image), because this matches the default table view created in the old days when we used NSCell-based table views. There are also standard cells in the IB object palette, one with just a text field, and one with an image view as well.


If your table cell starts out with a text field you don't need, for example when you want only a checkbox, then it's fine to just delete the text field. This is an extra step, but the cell contains a text field in the vast majority of cases, so it's a reasonable default for it to be there.


Also, it is not necessary to implement "tableView:objectValueForTableColumn:row:" (although you can if you want), because it's typically easier just to set the "objectValue" property of the cell directly in "tableView:viewForTableColumn:row", right after you make the cell.


Also, it is not necessary to set column identifiers explictly, because it's done for you if you don't. Instead of passing a literal string to "makeViewWithIdentifier:owner:", you can pass "tableColumn.identifier" instead, which uses the generated identifier. The only cases where you might want to set explicit column identifiers is when you want to test them explicitly inside "tableView:objectValueForTableColumn:row:". (With automatic identifiers, you don't know what to test against.)


Finally, you had a particular unpleasant experience because you wanted to do two things that are advanced topics in table views:


1. Dynamically adding columns that had no prototype in the original table view. This is quite doable, but is a bit of a head scratcher to do before you know how all the "moving parts" fit together. In particular, you may have to do manually some of the things that the table view normally does for you, such as creating an independent XIB file for the prototype cell, and registering its column identifier manually.


2. Binding cell values to data model properties whose names need to be calculated at run time ("field1", "field2", etc). This is also doable, but you have to be clear on what bindings are needed, and when to set them up.


Anyway, if you have it working now, congratulations!

How do I create a tableview with a variable number of columns where the extra columns have checkboxes in them?
 
 
Q