At the end of
part 3, we observed that there were some problems in my model design. Because the model does not truly affect cappuccino, I have made some changes to the underlying model, which I will show here with UML, and allow you to download from GitHub here: https://github.com/JaredClemence/CPTableView-Tutorial-Part-4---Starting-Code.git
|
We added two equation objects which replace the erroneous Skill and SkillSpecialty attributes. UML created in MagicDraw PE. |
|
The Skill and SkillSpecialty attributes are generalized into instances of a deeper extraction called Attribute. UML created in MagicDraw PE. |
Current State of Work
After making some changes to the base code, we reload our page, and we have the following:
|
Data Table After Modifications To the Code. Download The Starting Code From GitHub, and read "Current State of Work" to discover the changes that were made since the last part. |
What happened! Don't fret, this is actually what we expect, once we learn that we have changed the data source to return objects instead of strings. At the end of the last part, we noted that one of the problems with the design was that the Model elements were processing data to return a formatted string for the display, and we further noted that we were going to change the design so that we separate the View and Model elements more completely.
That is exactly what we have done. While changing the underlying model, we also changed the data source so that instead of returning processed strings, we just return an object itself.
The table does no processing with these objects, instead, it hands the objects over to the default view, and the default view fails to handle the objects the way that we would desire, so it instead calls the description method on the object and displays the string that is returned. Interestingly, at least to me, we see that the quality, which was returned as a CPArray, defaults to a comma separated list of it's contents. In this case, the array is a list of strings, so we end up seeing something very similar to what we initially intended when the Model was processing the data to create a similar output.
Creating Special Views
In this tutorial, we endeavor to truly divide the Model and View segments by creating special views that will process the
attack and
damage objects in their native object format instead of a modified "viewable" format. We see already what happens when the default view handles the display of our objects, but what can we do?
We are going to create special views for three of our four columns. At first, we are going to just create some blank views. Then we are going to just replicate the string output of the previous version; afterward, we will change the shape of the cells and add images to the design.
First, blank views
Let us create a file called WeaponTableViewElements.j. Into this file, we will create several CPView derivatives, for each of our columns. Because I have plans for the direction this tutorial will take down the road (alluded to in the model changes shown above), I have added another layer of separation between two of the views and the CPView parent. The Damage and Attack views will each rely on this secondary object, which each will use to produce special views later.
Into WeaponTableViewElements.j, add the following:
@import <Foundation/Foundation.j>
@import <AppKit/AppKit.j>
@implementation AttribListBasedDataView : CPView
{
AttributeList attributeList @accessors;
}
-(void)init
{
self = [super init];
if( self ){
attributeList = Nil;
}
return self;
}
@end
@implementation WeaponNameCellView : CPView
{
}
-(void)setObjectValue:(id)aWeapon
{
[self setBackgroundColor:[CPColor greenColor]];
}
@end
@implementation DamageTotalCellView : AttribListBasedDataView
{
}
-(void)setObjectValue:(id)valueObject
{
[self setBackgroundColor:[CPColor blueColor]];
}
@end
@implementation AttackRollCellView : AttribListBasedDataView
{
}
-(void)setObjectValue:(id)diceRoll
{
[self setBackgroundColor:[CPColor redColor]];
}
@end
@implementation QualityCellView : CPView
{
}
-(void)setObjectValue:(id)quality
{
}
@end
@implementation WeaponTrainingCellView : CPView
{
}
-(void)setObjectValue:(CPNumber)training
{
}
@end
Each of these view objects has a unique setObjectValue method. This is the method that the CPTableView will call to pass the object to the view that needs displaying. Because we don't actually do anything with the object, nor do we build our views, the cells will render as blank and empty areas. In order to add a little more effect to this phase of the tutorial, we do add some background color to our areas. We also will define our frame backgrounds differently in the next section to see what effect the frame has on the cell.
Next, we need to add our new views to our columns so that the columns offer them to the CPTableView on request. Make the following changes to AppController.j:
1. Include our new views by adding this to the import section:
@import 'WeaponTableViewElement.j'
2. Create empty instances of these views, and then insert them into the corresponding columns. Add the following code after the column definitions:
var weaponCellView = [[WeaponNameCellView alloc] initWithFrame:CGRectMake(0,0,100,200)],
attackCellView = [[AttackRollCellView alloc] initWithFrame:CGRectMake(0,0,100,100)],
damageCellView = [[DamageTotalCellView alloc] initWithFrame:CGRectMake(0,0,20,100)];
[weaponColumn setDataView:weaponCellView];
[attackColumn setDataView:attackCellView];
[damageColumn setDataView:damageCellView];
We have not even tested this part of the tutorial yet, and already I have started playing with elements in order to do two things: (1) teach a valuable lesson, and (2) speed up your understanding of CPTableView. Reload the page, and you will find this:
We changed the background color, and we see that each of our views is correctly being used
instead of the default views, except for quality which is still using the default view as before. We also made our template views of different frame sizes, but that seems to have had no effect. One possible thing that is going on here is that our view instance is not actually used in the table. In fact, this is true. Our instance is archived by the table view so that a template can be made, from that template many new instances are created, but it would seem that the frame is not the same as our original template.
A second possibility is that the cell, after it is created from our template, is forced to match the table's row and column format and that the cell has no control over it's own shape, location and dimensions, to find out, we will go back into our WeaponTableViewElements.j file and make two changes.
Add the following to setObjectValue method of WeaponNameCellView:
[self setFrame:CGRectMake(0,0,20,100)];
And add the following to setObjectValue method of DamageTotalCellView:
[self setFrame:CGRectMake(10,10,20,100)];
As the maker of this tutorial, I already knew that this would have an effect, but the results surprised me. What I initially expected to happen was that the green cell would still be confined to within the row in height, and it would appear to only be 20 units wide. The blue field would similarly be clipped into the cell area, but there would be a white border due to the (10,10) offset of the origin. What we actually see is this:
What we learn is that the cell views do have control over their own frame and origin, but surprisingly, we learn that they are each positioned with respect to the top left corner (the origin) of the clipped view in which the table forms.
We have not yet addressed this important fact, but what the TableView does with each cellView is calculates the origin that makes the new view appear to be in a table, and it sets that frame definition. After setting that frame definition, the CPTableView calls setObjectValue to allow the view to finish creating the data display. What we know from views already is that if we do not change the view's frame during our processing, anything that falls outside that view frame will be clipped.
Getting Our Strings Back
First, let's get our objects back to normal. Remove from our view definitions the background color and the special frames, and just to reinforce the fact that frames are reset by the table view, let's change our AppController so that all our views are created with CGRectMakeZero();
After making our changes, our CPTableView should look like this:
|
Our table view after we have removed the coloring and other formatting from the previous demonstration in this tutorial. |
Now that we have everything "back to normal", we can use what we have learned on previous tutorials to build string views. Every view will pretty much be the same for this trial, so we will create a helper object to create our view after we have used the object to create our text display. This is a temporary object, so we will create it in our WeaponTableViewElements.j file.
Add the following to our WeaponTableViewElements.j file:
The following will be added to setObjectValue of each indicated object class:
WeaponNameCellView:
var stringValue = [aWeapon name],
textField = [ViewHelper getTextDisplay:stringValue outerFrame:[self frame]];
[self addSubview: textField];
DamageTotalCellView:
var damageValue = [valueObject value],
textValue = Nil;
if( damageValue ){
textValue = damageValue.toString();
}else{
var skill = [valueObject skillName],
modifier = [valueObject modifier];
textValue = "";
if( skill ) textValue = skill;
if( modifier ){
if( modifier < 0 ){
var temp = modifier * -1;
textValue = textValue + " - " + temp.toString();
}else{
textValue = textValue + " + " + modifier.toString();
}
}
}
var textField = [ViewHelper getTextDisplay:textValue outerFrame:[self frame]];
[self addSubview:textField];
AttackRollCellView:
var testDiceNum = [diceRoll testDice],
bonusDiceNum = [diceRoll bonusDice],
modifier = [diceRoll modifier],
textValue = Nil;
if( testDiceNum || bonusDiceNum ){
textValue = "";
if( testDiceNum ) textValue = textValue + testDiceNum.toString() + "D";
if( bonusDiceNum ){
if( textValue != "" ){
textValue = textValue + " + ";
}
textValue = textValue + bonusDiceNum.toString() + "B";
}
if( modifier ){
if( modifier < 0 ){
var temp = modifier * -1;
textValue = textValue + " - " + temp.toString();
}else{
textValue = textValue + " + " + modifier.toString();
}
}
}else{
var skill = [diceRoll skillName],
specialty = [diceRoll specialtyName],
modifier = [diceRoll modifier];
textValue = "";
if( skill ) textValue = skill;
if( specialty ){
if( textValue != "" ){
textValue = textValue + " + ";
}
textValue = textValue + specialty + " Specialty";
}
if( modifier ){
if( modifier < 0 ){
var temp = modifier * -1;
textValue = textValue + " - " + temp.toString();
}else{
textValue = textValue + " + " + modifier.toString();
}
}
}
var textField = [ViewHelper getTextDisplay:textValue outerFrame:[self frame]];
[self addSubview:textField];
Reload your application and see the following:
Our application on first look is exactly the same as our previous incarnation in the last tutorial. However, on closer looks under the hood, we see that the Model objects no longer have anything to do with the display of data. They provide data as strings, numbers and other objects, and from there, our view objects grab the data that is needed and orient it for display.
For our experiment, we pass a Weapon object to the first column, a SkillBasedData object to the second column, a DiceEquation object to the third column and a CPArray object to the fourth column. Because our custom views handle the processing, we just as easily could pass the weapon to every column cell and let the cell extract from the weapon what is needed. It is in our dataSource that we get to choose how and what we are going to communicate with the view.
If you read the code very closely, you will see there is far too much code to just display what we are showing here. In fact, the damage and the attack views process their objects twice. First, they check the objects to see if one set of values can be retrieved, and on failure, they default to displaying a generic text representation that matches our previously expected text. This is one step closer to what we were alluding earlier in this tutorial. Later, we are going to learn to use the delegate function to customize displays even further. One table will display in our standard format, and the second table will display data differently as a result of the delegate.
In fact, we have already alluded to how we would add images and other effects to the inside of a single table cell, and exploring delegates sounds like fun, so let's do that now, and we will add visual character to our table in a few moments.
One more important note! If you attempt to move the columns, you will notice that the table calls setObjectValue on the existing view, which means that the view overwrites itself, which does not look good. As a result, we must be cognizant when building our view to always ensure that we start with a fresh view palate. I do this by calling [self setSubviews:[]]. For this small application, there is nothing wrong with this behavior. Ideally, you can save a lot of processing steps by reusing the text fields that are already set; save a reference to the items that will change, and if they exist already, just change the text values other visual references of those specific items instead of rendering the view all over again.
Using Delegates
In the very first part of this tutorial, when we reviewed Mr. Connor Denman's work, I noted that he sets a delegate for his table, but he never uses it, and I further claimed that the line of code could be removed and there would be no effect to his work. In fact, you will see if you review what we have done so far, we have not once used a delegate for our table view...until now.
In Cocoa, we are told that a delegate can be used to control the behavior of the table. The data view controls the view cell that the table places, and the delegate changes the way that the table performs various functions. What I aim to do is to change our table view so that we can link it to a character's attribute list and change how our special cells display.
Ultimately, if the table is linked to an attribute list, we will see a more practical display of information. For the damage, we will see a single number reflecting the amount of damage performed, and for the attack roll, we will see the number of dice and bonus dice required. These new values will be based on the character's specific values, and if the character list is changed or if the list is pointed to a new list, then the values change. Similarly, if the link is removed, the table reverts to the generic display we see now.
For a full list of delegate functions, please see the post titled "
CPTableView: A Ponderings and Guesses Regarding Delegate Functions". We are going to make use of a function called tableView:willDisplayView:forTableColumn:row:
Create a file called Delegate.j and insert the following code:
@import <Foundation/Foundation.j>
@implementation TableEnhancer : CPObject
{
CharacterProfile aCharacter @accessors;
}
-(void)tableView:(CPTableView)aTable willDisplayView:(id)aCell forTableColumn:(CPTableColumn)aColumn row:(CPInteger)aRowIndex
{
if( [aCell respondsToSelector:@selector( setAttributeList: ) ] ){
[aCell setAttributeList: [aCharacter attributes]];
}
}
@end
Also, we need to make some small changes to our DamageTotalCellView and our AttackRollCellView classes. Make the following changes in WeaponTableViewElements.j:
- change the function name from setObjectValue: to rebuildView: in the two classes indicated above: DamageTotalCellView and our AttackRollCellView.
- Add the line [self setSubviews:[]]; to the beginning of each of your new rebuildView: methods.
- In the base class, add the function setObjectValue and setAttributeList as follows:
-(void)setObjectValue:(id)anObject
{
aSavedObject = anObject;
var specialPath = NO;
if( [anObject respondsToSelector:@selector( setAttributeList: )] ){
specialPath = YES;
[anObject setAttributeList:attributeList];
}
[self rebuildView:anObject];
if( specialPath ){
[anObject setAttributeList:Nil];
//clear the attribute list, just in case the value is being provided to multiple sources. Probably not necessary, but what the heck.
}
}
-(void)setAttributeList:(AttributeList)aList
{
//override accessor function
attributeList = aList;
[self setObjectValue: aSavedObject];
}
To the AppController.j file, make the following changes:
1. Add the following to the import section:
@import 'Delegate.j'
@import 'CharacterProfile.j'
2. Somewhere after tableView is defined in applicationDidFinishLaunching, add the following:
var aDelegate = [[TableEnhancer alloc] init],
aCharacter = [[CharacterProfile alloc] init];
[aDelegate setACharacter:aCharacter];
[tableView setDelegate:aDelegate];
Now, reload your project, take a look at what you have gotten. Then edit AppController.j and comment out the line "
[aDelegate setACharacter:aCharacter];". Reload the page again. You should see the following in each of the page reloads:
|
When a character is assigned to our Delegate, the delegate changes the table by filling in character specific values. |
|
When the Delegate has no Character Profile, it makes no changes and the generic information is displayed. |
Amazing! Using the delegate functions, we are able to make the information more specific to the user depending on whether a character profile is loaded. Furthermore, we can "unload" the character profile, and our customized views handle the change perfectly by defaulting to a generic display of data. This is all done without any change to our view or to our data. It is just how the view is designed to operate as a default behavior.
When I change the default values for affected attributes or specialties, I change the resulting table. Play with CharacterProfile.j using the generic function data to interpret the Attribute and Specialty names. I'm going to change several values, and reload the page.
There is a lot of power in this ability to combine data sources and delegates with the underlying base classes in order to change functionality. Because the delegate is not a subclass of the tableView, we can even offer a list of delegates to our user so that the user can choose a more custom view simply by selecting a delegate preference. Of course, our user will not know that is what he or she is doing, but for us, it means that we can free ourselves from redesigning tables every time that we need a slightly altered view.
Getting rid of dead space
Our CPTableView is getting closer and closer to a decent looking display. I have not yet found a way to use the delegate to effectively cause a dynamic resizing of the columns and rows, which will be important as we play with images and other aspects that will go beyond the tables row and column width. However, during that search I did stumble on two settings that help to fill the scroll area with column, so that we don't have the dead space to the right.
Calling the function
setColumnAutoresizingStyle allows us to set one of three style settings. The first is the default, so we will not cover it, the other two are CPTableViewFirstColumnOnlyAutoresizingStyle a
nd CPTableViewLastColumnOnlyAutoresizingStyle. Pass either of these constants to the table view via the function listed, and you will expand either the first or last column to fill the entirety of the space so that the table now looks like this:
|
The Qualities column is now stretched to fill the entire scroll area. Note, the stretch happens regardless of whether the underlying views are clipped or not. These styles do not cause all columns to find an optimum width to display the most unclipped content possible. |
Adding Some Graphics
We have done a lot with our tables, but so far, our table still just contains string data, and it looks very plain. What if I wanted to add buttons or images or some other view item to the cell area? How to I ensure that the extra image data or control data is not clipped by the cell's div element?
I have already shown you that we can create custom views, and on previous tutorials, you have seen that with a view, we can add as many subviews as we like to fully customize our area within the indicated frame or content bounds.
If you downloaded the starter code at the beginning of this tutorial, then you already have the images that we will use, which I modified from icons I found on Deviant Art and designed by user 7Soul1. These are not icons I would use in a personal design, but they meet our needs perfectly, and they are not bad at all.
Let's make some changes now:
1. In AppController.j, add the following somewhere between the point that the columns are initialized and the point where they are added to the tableView:
[damageColumn setWidth:50];
[attackColumn setWidth:70];
[qualityColumn setWidth:150];
[tableView setRowHeight: 50];
2. Let's alter our DamageTotalCellView in WeaponTableViewElements.j, we keep the bulk of the code, but there is sufficient changes that I will just add the whole implementation here:
@implementation DamageTotalCellView : AttribListBasedDataView
{
Weapon aWeapon;
}
-(id)init
{
self = [super init];
if( self ){
aWeapon = Nil;
}
return self;
}
-(void)setObjectValue:(id)weapon
{
if( [weapon class] == "Weapon" ){
aWeapon = weapon;
[super setObjectValue:[weapon damage]];
}else{
[super setObjectValue:weapon];
}
}
-(void)rebuildView:(id)valueObject
{
[self setSubviews:[]];
var damageValue = [valueObject value],
textValue = Nil;
if( damageValue ){
textValue = damageValue.toString();
}else{
var skill = [valueObject skillName],
modifier = [valueObject modifier];
textValue = "";
if( skill ) textValue = skill;
if( modifier ){
if( modifier < 0 ){
var temp = modifier * -1;
textValue = textValue + " - " + temp.toString();
}else{
textValue = textValue + " + " + modifier.toString();
}
}
}
var textField = [ViewHelper getTextDisplay:textValue outerFrame:[self frame]];
if( damageValue ){
//lets put an image here to make our number look less lonely.
var imageView = [self getImageForItem:aWeapon];
if( imageView ){
var imageFrame = [imageView frame],
rightImageEdge = imageFrame.origin.x + imageFrame.size.width,
leftTextEdge = rightImageEdge + 5,
textFrame = [textField frame];
textFrame.origin.x = leftTextEdge;
[textField setFrame:textFrame];
[self addSubview:imageView];
}
}
[self addSubview:textField];
}
-(CPView)getImageForItem:(Weapon)weapon
{
if( !weapon ) return Nil;
var attackRoll = [weapon attack],
primarySkill = [attackRoll skillName];
if( !primarySkill ) return Nil;
var filePath = @"Resources/" + primarySkill + @".png",
image = [[CPImage alloc] initByReferencingFile:filePath size:CGSizeMake(35,35)],
imageView = [[CPImageView alloc] initWithFrame:CGRectMake(10,15,20,20)];
[imageView setImage:image];
return imageView;
}
@end
3. Lastly, let's change our data source so that it provides the whole weapon for the damage column instead of just the damage attribute. In WeaponTabledataSource.j we change our switch block to the following:
switch(colId)
{
case 'Weapon':
return weapon; //Weapon
case 'Training':
return [weapon training]; //CPInteger
case 'Attack':
return [weapon attack]; //DiceEquation
case 'Damage':
return weapon; //Weapon
case 'Quality':
return [weapon quality]; //CPArray
default:
return "ERROR!";
}
Reload your project and find the following:
|
Our new view scrolled to the bottom and one row selected. |
The changes we made to AppController really were gratuitous. I wanted to show you how to change the size of each row, and also that we can define the width of the columns upfront. By doing this, we reduce the wasted space and make the data appear much more natural in its positions.
To the damage cell and the data provider we made some other adjustments. I wanted to make that damage number look a little less lonely, so I added a simple graphic, which changes based on the primary skill of the attack roll for the weapon selected. To do this, the cell needs to have access to the weapon, and not just the damage value, so that it can access both the damage and the attack attributes. In this example, I use a sword for "Fighting" skill and a bow for the "Marksmanship" skill. In retrospect, the "Fighting" skill would probably have better been served by an image of a fist, because it looks strange to see a sword on a line attributed to a non-edged weapon like the "Whip". Or, even better yet, I could have a unique image for each weapon and instead of showing the primary skill, I could have just shown an image of the weapon itself. The point is not what I chose to display, but that I have FULL control over my table cells and I can build whatever it is that I desire using these tools.
Some people may wonder why there is only three rows in my captured image. Don't worry! The four rows are all present and accounted for. The new row height was too large to allow all the rows to display within the scroll view, so I had to scroll to the bottom of the list to display the bow and the sword icons together. The fourth weapon is above the scroll view forming what appears to be a margin at the top of the table; it's not a margin; it's just the bottom of a row that is clipped.
There is much more to learn about table views, for example, we have not looked at handling selection events, turning on multiple selection, drag-n-drop event handling and much, much more. However, I hope that this has been sufficient to at least get you started on your exploration. We will continue to add more notes on these objects and views as I have a need to explore them further, but for the time being, I have spent enough of my time on this particular view element and will move on to another aspect of our Cappuccino education.
You can download the final code using git with the following URL:
https://github.com/JaredClemence/CPTableView-Tutorial-Part-4---Final-Code.git
One Last Thing - Reloading Data
After posting this article, I realized that there is so much that was not yet covered. On one of my work projects, I could not figure out why the table would not update data as the data was changed. The error turned out to be with the programmer, who updated the wrong data object (thus the table had nothing to change).
Regardless what caused the error, it made me realize that through the course of this tutorial, we never once addressed forcing the table to reload the data. Many CPTableView functions call various other functions within the table to cause it to update its view. If ever you have a need to redisplay the data, you can call "[yourTableVariable reloadData];". This method call will cause the table to fetch all records again, lay out the table again, and call the delegate functions again. There is no need to use methods like setNeedsLayout or setNeedsDisplay:YES on the table when you are using the reloadData method.
I hope this information is helpful. Have fun with your tables, and if you have any great examples or lessons to share, please do so in the comments below.