NSCell Subclass + custom Bindings

  • NSCell Subclass + custom Bindings

    Hallo Götter der Sonne,

    Problemstellung:

    Habe eine NSTableView. Jede "Zeile" repräsentiert eine Stofftüte. Jede Stofftüte kann ein Bild (hasImage) haben und einen Sound (hasSound). Nun möchte ich eine Spalte in der TableView haben, die mir ein kleines Bild malt, falls die entsprechende Stofftüte ein Bild und/oder einen Sound hat - also praktisch eine kleine "Statusanzeige".

    Nun nichts leichert als das - NSCell-Subclass gezaubert und zwei bindings exposed:

    Quellcode

    1. + (void)initialize
    2. {
    3. [self exposeBinding:@"hasImage"];
    4. [self exposeBinding:@"hasSound"];
    5. }


    Die dataCell der Spalte binde ich im Code.

    Starte ich die Anwendung erhalte ich folgenden Fehler:

    Quellcode

    1. 2007-01-04 23:34:11.452 Ebbinghaus[3564] *** -[NSCFArray charValue]: selector not recognized [self = 0x331f60]
    2. 2007-01-04 23:34:11.453 Ebbinghaus[3564] *** NSRunLoop ignoring exception '*** -[NSCFArray charValue]: selector not recognized [self = 0x331f60]' that raised during posting of delayed perform with target 3a6980 and selector 'reconfigureCardsTableView:'
    3. 2007-01-04 23:34:11.461 Ebbinghaus[3564] *** -[NSCFArray charValue]: selector not recognized [self = 0x3f4b70]
    4. 2007-01-04 23:34:11.461 Ebbinghaus[3564] *** NSRunLoop ignoring exception '*** -[NSCFArray charValue]: selector not recognized [self = 0x3f4b70]' that raised during posting of delayed perform with target 3a6980 and selector 'delayedSetup'


    Ich weiß auch schon an was das liegt - in meinem NSCell subclass:

    Quellcode

    1. static void *HasImageObservationContext = (void *)2091;
    2. static void *HasSoundObservationContext = (void *)2092;
    3. @implementation CKMediaCell
    4. [...]
    5. - (void)bind:(NSString *)bindingName toObject:(id)observableController withKeyPath:(NSString *)keyPath options:(NSDictionary *)options
    6. {
    7. if ([bindingName isEqualToString:@"hasImage"]) {
    8. [observableController addObserver:self forKeyPath:keyPath options:nilcontext:HasImageObservationContext];
    9. }
    10. if ([bindingName isEqualToString:@"hasSound"]){
    11. [observableController addObserver:self forKeyPath:keyPath options:nil context:HasSoundObservationContext];
    12. }
    13. // DARAN:
    14. [super bind:bindingName toObject:observableController withKeyPath:keyPath options:options];
    15. }
    16. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    17. {
    18. if (context == HasImageObservationContext) {
    19. [self setHasImage:YES];
    20. }
    21. if(context == HasSoundObservationContext) {
    22. [self setHasSound:YES];
    23. }
    24. }
    Alles anzeigen


    Wieso? :(
    Die Objective-Cloud ist fertig wenn sie fertig ist. Beta heißt Beta.

    Objective-C und Cocoa Band 2: Fortgeschrittene
    Cocoa/Objective-C Seminare von [co coa:ding].
  • RE: NSCell Subclass + custom Bindings

    ICH DULDE KEINEN ZWEITEN GOTT NEBEN MIR!!!!!!!!!!!

    Deine Superklasse kennt die Bindings naturgemäß nicht. Entweder du machst eine if-else-Kaskade oder in den jeweiligen if-Zweigen ein return.
    Es hat noch nie etwas gefunzt. To tear down the Wall would be a Werror!
    25.06.2016: [Swift] gehört zu meinen *Favorite Tags* auf SO. In welcher Bedeutung von "favorite"?
  • So:

    Quellcode

    1. - (void)bind:(NSString *)bindingName toObject:(id)observableController withKeyPath:(NSString *)keyPath options:(NSDictionary *)options
    2. {
    3. if ([bindingName isEqualToString:@"hasImage"]) {
    4. [observableController addObserver:self forKeyPath:keyPath options:nil context:HasImageObservationContext];
    5. [super bind:bindingName toObject:observableController withKeyPath:keyPath options:options];
    6. return;
    7. }
    8. if ([bindingName isEqualToString:@"hasSound"]){
    9. [observableController addObserver:self forKeyPath:keyPath options:nil context:HasSoundObservationContext];
    10. [super bind:bindingName toObject:observableController withKeyPath:keyPath options:options];
    11. return;
    12. }
    13. // DARAN:
    14. //[super bind:bindingName toObject:observableController withKeyPath:keyPath options:options];
    15. }
    Alles anzeigen


    oder so?

    Quellcode

    1. - (void)bind:(NSString *)bindingName toObject:(id)observableController withKeyPath:(NSString *)keyPath options:(NSDictionary *)options
    2. {
    3. if ([bindingName isEqualToString:@"hasImage"]) {
    4. [observableController addObserver:self forKeyPath:keyPath options:nil context:HasImageObservationContext];
    5. return;
    6. }
    7. if ([bindingName isEqualToString:@"hasSound"]){
    8. [observableController addObserver:self forKeyPath:keyPath options:nil context:HasSoundObservationContext];
    9. return;
    10. }
    11. // DARAN:
    12. [super bind:bindingName toObject:observableController withKeyPath:keyPath options:options];
    13. }
    Alles anzeigen


    Wohl weder noch, sonst würde es funktionieren.

    Im ersten fall spuckt er mir wieder eine Fehlermeldung aus von wegen:

    2007-01-05 09:13:49.326 Ebbinghaus[4371] *** -[NSCFArray boolValue]: selector not recognized [self = 0x3347e0]
    2007-01-05 09:13:49.326 Ebbinghaus[4371] *** -[NSCFArray boolValue]: selector not recognized [self = 0x3347e0]


    Im zweiten Fall ist in - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context

    das bindinginfo für meine zwei bindings immer null, da sie scheinbar nicht gebunden sind, da das return zu früh kommt.
    Die Objective-Cloud ist fertig wenn sie fertig ist. Beta heißt Beta.

    Objective-C und Cocoa Band 2: Fortgeschrittene
    Cocoa/Objective-C Seminare von [co coa:ding].
  • Du verstehst das falsch: Die Bindung selbst wird nicht von der Supermethode vorgenommen. -bind:toObject: (super) gehört daher nicht dahin, wenn du ein eigenes Binding implementierst. Daher enthält die erste version den gleichen Fehler, den auch deine Ursprungsversion enthält.

    Die Zweite ist vom Ansatz her richtig. Natürlich hast du kein infoForBinding, da du das Binding ja selbst einrichtest. Du kannst dir Pfad und Objekt ja als Instanzvariablen speichern. Wenn du willst, überschreibst du dir inforForBinding: in deiner Subklasse und gibst da deine Instanzvariablen in einem Dictionary zurück. Aber eigentllich benötigen das vor allem Subklassen deiner Klasse, nicht du selbst. (Du kennst ja deine Instanzvariablen.)

    Such mal im Bindings-Tutorial, dort findet sich ein Beispiel, gleich mit Transformer.
    Es hat noch nie etwas gefunzt. To tear down the Wall would be a Werror!
    25.06.2016: [Swift] gehört zu meinen *Favorite Tags* auf SO. In welcher Bedeutung von "favorite"?
  • Habe mir nochmals alles genau angeschaut und ich hänge immernoch...

    Tom schreibt: "Die Bindung selbst wird nicht von der Supermethode vorgenommen."
    Klar - logisch. Habe nun super durch self ersetzt.

    Momentan sehe ich das Hauptproblem darin, dass ich zwar informiert werde, sobald sich ein Wert für hasSound/hasImage ändert - aber den neuen bzw. alten Wert kann ich leider nicht ermitteln. Hier mal mein Ansatz:

    Quellcode

    1. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    2. {
    3. if (context == HasImageObservationContext) {
    4. // HasImage Change
    5. }
    6. if(context == HasSoundObservationContext) {
    7. // HasSound Change
    8. }
    9. }


    Habe mal alle Parameter geNSLogged aber den neuen Wert konnte ich nicht ermitteln...

    Tom schreibt: "Du kannst dir Pfad und Objekt ja als Instanzvariablen speichern."

    Ich habe sowas schonmal in einem Beispielcode gesehen - irgendwo bei cocoabuilder/cocoadev - finde es aber momentan nichmehr - leider.

    Anhand deines Tutorials konnte ich mein Problem leider auch nicht lösen.

    Mein Herz würde sich sehr erfreuen, falls du dich nochmals mit meinem Problem beschäftigen könntest.

    :)
    Die Objective-Cloud ist fertig wenn sie fertig ist. Beta heißt Beta.

    Objective-C und Cocoa Band 2: Fortgeschrittene
    Cocoa/Objective-C Seminare von [co coa:ding].
  • Tom schreibt: "Die Bindung selbst wird nicht von der Supermethode vorgenommen."
    Klar - logisch. Habe nun super durch self ersetzt.

    Das kann nicht sein, da du dir dann einen veritablen Stack-Overflow programmiert hättest. Es gibt genau einen Aufruf an super: Am Ende deiner Methode und nur dann, wenn du nicht ein "eigenes" Binding gefundest hast.

    Momentan sehe ich das Hauptproblem darin, dass ich zwar informiert werde, sobald sich ein Wert für hasSound/hasImage ändert - aber den neuen bzw. alten Wert kann ich leider nicht ermitteln.

    Die Werte werden je nach Option in dem change-Dictionary übergeben. Dazu musst du aber bei der Observierung angeben, welche du haben willst. Aber in der Regel benötigst du das auch nicht.

    a) Bei einer Observierung ist es guter Stil, seinen eigenen Setter für die gebundene Eigenschaft zu bedienen:

    Quellcode

    1. if (context == HasImageObservationContext) {
    2. // HasImage Change
    3. oldValue = [self boundAttribute];
    4. [self setBoundAttribut:newValue];
    5. }
    Dadurch gewinnst du zwei Vorteile: 1. musst du nicht in der drawRect: oder sonstwo, wo du den Wert benötigst, jedesmal das ganze Binding auflösen. 2. kannst du dann deine Klasse auch mittels der normalen Accessoren anstelle der Bindings verwenden. Das gibt es ja auch noch.

    b) Den neuen Wert kannst du anhand des Bindings feststellen:

    Quellcode

    1. newValue = [[self boundObject] valueForKeyPath:[self boundKeyPath]];


    Tom schreibt: "Du kannst dir Pfad und Objekt ja als Instanzvariablen speichern."

    Ich habe sowas schonmal in einem Beispielcode gesehen - irgendwo bei cocoabuilder/cocoadev - finde es aber momentan nichmehr - leider.

    Graphics Binding oder so müsste es als Example geben. Auch in dem Tutrial ist dazu Code vorhanden. Es ist etwas versteckt, ich glaube, zweites Kapitel ganz am Ende.

    Bei der Erstellung des Bindings (-bind:toObject…) machst du einfach:

    Quellcode

    1. [self setBoundObject:object];
    2. [self setBoundKeyPath:keyPath];


    Du merkst dir also einfach, was du gebunden hast. Der Pfad für KVC und KCO ist ja derselbe.

    Wenn du willst, kannst du dir auch ein -infoorForBinding: schreiben:

    Quellcode

    1. - (NSDictionary*)infoForBinding:(NSString*)binding {
    2. if( [binding isEqualToString:@"myBinding"] ) {
    3. return [NSDictionary dictionaryWithObjectsAndKey:…, nil];
    4. }
    5. return [super infoForBinding:binding];
    6. }


    Man kann gar nicht häufig genug daran erinnern, dass Bindings nichts Neues sind, sondern lediglich ein Interface für KVO bilden. Es "gibt" keine Bindings-Technologie, wenn du so willst.
    Es hat noch nie etwas gefunzt. To tear down the Wall would be a Werror!
    25.06.2016: [Swift] gehört zu meinen *Favorite Tags* auf SO. In welcher Bedeutung von "favorite"?
  • Hallo Tom,

    es ist zum verzweifeln.

    Ich habe mir das Graphics Binding Beispiel wieder angeschaut - genau das ist es, was ich damals auch gesehen habe. Habe nun alles so umgebaut wie du es beschrieben hast:

    @interface hat nun zwei neue Members:

    Quellcode

    1. id hasImageBoundObject;
    2. NSString *hasImageKeyPath;


    Später für den Sound nochmal das selbe.

    Dann:

    Quellcode

    1. - (void)bind:(NSString *)bindingName toObject:(id)observableController withKeyPath:(NSString *)keyPath options:(NSDictionary *)options
    2. {
    3. if ([bindingName isEqualToString:@"hasImage"]) {
    4. [observableController addObserver:self forKeyPath:keyPath options:nil context:HasImageObservationContext];
    5. hasImageBoundObject = observableController;
    6. [self setHasImageKeyPath:keyPath];
    7. return;
    8. }
    9. (...)


    Den neuen Wert:

    Quellcode

    1. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    2. {
    3. if (context == HasImageObservationContext) {
    4. // HasImage Change
    5. NSLog(@"new value: %@", [hasImageBoundObject valueForKeyPath:[self hasImageKeyPath]]);
    6. }


    Ich binde die dataCell wie folgt:

    Quellcode

    1. [[tableColumn dataCell] bind:@"hasImage" toObject:cardsArrayController withKeyPath:@"arrangedObjects.hasImage" options:nil];


    Dieser new value gibt mir nicht nur den neuen Wert sondern ein Array aller Werte.

    Sag bloß ich muss noch NSTableColumn mit meiner Cell bekanntmachen...
    Die Objective-Cloud ist fertig wenn sie fertig ist. Beta heißt Beta.

    Objective-C und Cocoa Band 2: Fortgeschrittene
    Cocoa/Objective-C Seminare von [co coa:ding].
  • Jein, zwei Fehler. Aber erst einmal gut, dass ich jetzt verstanden habe, worum es genau geht.

    a)
    Also, du bindest ja ein Array an die Cell. Das kann nicht gutgehen. Normalerweise werden ja Spalten gebunden, also Mehrheiten von Cells.

    b)
    Na ja, die Table-Column kann ja nun nicht von sich aus ahnen, dass sie eine andere Cell verwenden soll.

    Ich glaube, schlag mich bitte nicht!, du benötgst gar kein eigenes Binding. Du willst ja einen Wert, etwa hasImage, anders darstellen. Dazu kannst du ja das normale value-Binding nehmen. Wenn du das Table-View ganz normal bindest, bekommt es ja automatisch des Wert zugewiesen. Also, um es systematisch und kurz zu machen:

    Der Table-View hängt an einem Array-Controller und zwar an demjenigen, den auch die Spalten haben. Für jede Zeile holt der Array-Controller das entsprechende Objekt. Darauf wendet es den Key-Path aus dem Spaltenbinding an. Das ist leider im IB (und auch schon in Cocoa selbst) extrem missverständlich, weil man mehrere Array-Controller an verschiedene Spalten binden könnte, was offenkundig Unfug ist. Ganz schlecht gelöst von Apple!

    Damit bekommt jede Cell automatisch den richtigen Wert.

    Jetzt wilst du aber möglicherweise zwei Werte (hasSound und hasImage) in einer Cell kombinieren? Das wird frickelig. Ohne jetzt das halbe Cocoa neu zu programmieren, würde ich entweder auf zwei Spalten ausweichen oder aber die beiden Eigenschaften in einem Pattern kombinieren. Dazu machst du dir einfach in deinem Model eine neue, abgeleitete Eigenschaft combinedAttribute, die du auf 0 bis 3 setzt, wenn hasImage oder hasSound verändert werden. In deiner Cell fragst du dann diese Werte wieder ab.

    Aber dem Table-View jetzt beizubringen, dass es zwei Werte setzen muss, ist mega-umständlich.

    Ich bin mir aber nicht sicher, ob ich dich jetzt richtig verstanden habe. Kannst du mir dein Ziel noch einmal erklären?
    Es hat noch nie etwas gefunzt. To tear down the Wall would be a Werror!
    25.06.2016: [Swift] gehört zu meinen *Favorite Tags* auf SO. In welcher Bedeutung von "favorite"?
  • Tom - DOPPELBINGO! :)

    Genau das habe ich versucht - aus zwei Spalten eine machen.

    Das von dir beschrieben umwandeln des values kam mir auch schon in den Sinn - hielt ich aber im ersten Moment für die problematischere Lösung.

    Nun gut - wieder was gelernt.

    Werde nun einfach statt zwei bools ein int im wertebereich 0-3 speichern und die cell damit arbeiten lassen.

    Nochmal danke, dass du dir mit mir so viel Mühe gegeben hast.

    Als dank kauf ich dein buch :)
    Die Objective-Cloud ist fertig wenn sie fertig ist. Beta heißt Beta.

    Objective-C und Cocoa Band 2: Fortgeschrittene
    Cocoa/Objective-C Seminare von [co coa:ding].