NSArrayController für ein NSDictionary

  • NSArrayController für ein NSDictionary

    Auch wenn es schon spät ist:
    Meine Dokumentenklasse enthält ein NSDictionary. Dessen Inhalt möchte ich ein einem NSTableView und in einem NSPopupButton darstellen.

    Ich habe mir gedacht, eine Subklasse von NSArrayController zu erstellen, die aus dem NSDictionary eine NSArray macht (geht ja ganz einfach mit [dict allValues]).

    Nun zur Strategie: Im IB den eigenen ArrayController instanziieren und "contentArray" mit dem NSDictionary des Documents verbinden. Da "contentArray" eigentlich mit einem NSArray-Object gebunden sein soll (und nicht mit meinem NSDictionary), kann diese KVO-Verbindung nur zum Benachrichtigen von Änderungen benutzt werden.
    Um erstmal simpel anzufangen habe ich "arrangedObjects" in meiner NSArrayController-Klasse überschrieben:

    Quellcode

    1. - (NSArray*) arrangedObjects: (NSArray*) objects
    2. {
    3. return [[[self document] kindStatistics] allValues];
    4. }
    (der ArrayController besitzt eine Methode "document")

    Leider funkltioniert es nicht, denn nachdem diese Methode aufgerufen wurden, erscheint eine Fehlermeldung, dass aus einem Dictionary kein Array erstellt werden kann. Es scheint also von woanders aus auf das Model des Controllers zugegriffen zu werden.

    Die Frage lautet also: Wie implementiert man einen ArrayController für ein NSDictionary?

    Vielen Dank für Anregungen im Voraus,
    Tjark

    Update: Es funktioniert nur, wenn ich im IB als "Model Key Path" (dieser gibt ja das NSDictionary an) "allValues" anhänge, so dass der ArrayController gleich mit einem NSArray-Objekt versorgt wird. Funktioniert dann KVO bzgl. Änderungen am Dictionary noch?
  • Wenn es sich nur um ein Dict handelt, würde ich den Object-Controller hernehmen. Aber du möchtest ja an ein TableView binden. Versuch deine Datenstruktur umzustellen und erstelle ein Array das mit Dicts gefüllt wird. Das kannste dann als Content für den ArrayController verwenden.
  • RE: NSArrayController für ein NSDictionary

    Ich kenne von meinem PostgreSQL - Framework das Problem, dass man einen Zugriff sowohl mittels Index als auch mittels Key haben möchte.

    Ich halte es nicht für eine gute Idee, den Array-Controller zu subclassen. Bedanke: Da gbt es Methoden wie add, die keinen Key haben. Was willst du da einfügen. Das passt nicht so recht.

    Icvh habe das Problem gelöst, indem ich das Model beide Strukturen anbiete. Ich habe zum Beispiel im Model einen Key "tables", welches einen indizierten Zugriff für einen NSArrayController bietet. Dann erzeuge ich "parallel" ein Dictionary, welches alle Tabellen als Key enthält, so dass man ebenfalls mit dem Schlüssel "tablesByName.Adresstabelle" per Key an eine bestimmte Tabelle herankommt. Die Erzeugung ist übrigens nicht schwierig. In einem ersten Ansatz kannst du einfach jedesmal das Dictionary neu aufbauen. Wenn du es granularer haben willst, bietet sich eine Array-Subtraktion an, wie man sie etwas bei 1:n-Bindings macht.
    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"?
  • Verstehe. Nur leider ist ein NSArray nicht immer die beste Collection-Form für ein bestimmtes Problem....

    Hier eine weitere Idee:
    Zwischen dem Daten-Model, dass eine Collection ist (NSDictionary, NSSet), und dem NSArrayController einen ObjectController schalten, der folgende Eigenschaften hat:
    "content" des ObjectControllers zeigt auf ein Object, das einen indexierten Zugriff auf das Collection-Object ermöglicht (also "objectAtIndex" und "count" implementiert).
    Dieses Indexierungs-Objekt hat ein NSEnumerator für die Collection, um durch das zugrundeliegende Daten-Model zu toben.
    Zusätzlich speichert er noch den zuletzt angefragten Index, so dass er bei lineraren Index-Anfragen nur [_enum nextObject] aufzurufen braucht.
    Nur bei nicht-lineraren Zugriffen wäre das recht ineffizient, da er evtl. immer von vorne durchlaufen muss.
    Wenn sich sich das Daten-Model geändert hat (der NSObjectController wird ja über KVO benachritigt), erzeugt er einen neues Indexierungs-Object, was wieder der NSArrayController über KVO mitgekommt und daraufhin z.B. ein NSTableView aktualisert.

    Durch diesen Meachanismus kann für das Daten-Model die beste Collection-Klasse gewählt werden und trotzdem in einem NSTableView dargestellt werden (dieser Aufwand ist natürlich überflüssig, wenn das Daten-Model 'eh ein NSArray ist).
  • Ich weiß nicht, ob ich dich richtig verstanden habe, daher mit Vorsicht:

    Jo, du kannst die auch verketten. Aber wozu der Aufwand?

    Bedenke Folgendes:
    - Wenn du ein Dict hast, klappt der Zugriff mit Bindings nur über existierende Keys. Diese müssen in dem Dict vorhanden sein.

    - Da der Key in einer Array-Präsentation verloren geht, muss er in dem Objekt selbst nochmal vorhanden sein (oder er ist nicht notwendig, was aber die Ausnahme darstellen dürfte.)

    - Die Repräsentationen sollten doch beide in dem Model vorhanden sein? Immerhin wirst du sie häufiger benötigen. Warum teilen? Außerdem dürfte das Model bei vielen Routinen besser entscheiden können, was genau zu aktualisieren ist.

    Dann mach es doch einfach so, dass du in dem Model einen Schlüssel contentsArray implementierst, der wie folgt aussieht:

    Quellcode

    1. - (NSArray)contentsArray {
    2. return [contentsDict allObjects];
    3. }


    Halt so, wie du es vorhattest, aber ohne den ArrayController zu überladen. Ist doch viel einfacher!?

    Ein Problem bleibt und das ist ein echter Nachteil, allerdings auch eine Schönheitsfrage.

    -add(ArrayController) legt einfach über das init ein neues Element an. Das hat dann keinen passenden Key -- naturgemäß. Du kannst dieses Objekt also nicht in deinem Dict speichern.

    Ich habe dieses Problem gelöst, indem ich eine Add-Methode im Model anbiete, die natürlich nachschauen kann, was frei ist. Da wird dann etwa der Key "table5" vergeben. So kann ich das auch im Dict speichern.

    Nicht schön, aber funktioniert. Aus verschiedenen Gründen muss ich früher oder später wohl mal die Funktionialität in dem ArrayController unterbringen, wie es ja auch dir vorschwebt. Aber zunächst einmal benötigt man diese Arbeit nicht.
    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"?
  • Ist schon richtig - mir ging es es eher um eine generische Lösung ohne das Model an die Bedürfnisse von NSTabelView anpassen zu müssen.

    Ich habe es jetzt probehalber erstmal so gemacht, dass sich der NSArrayController mit "document.content.allValues" verbindet.

    Das funktioniert auch, nur wird das wahrscheinlich nicht mehr gehen, wenn das Dokument Änderungen an "content" mittels KVO mitteilen möchte, oder?
    (weil sich der ArrayController mit "allValues" vom "content"-Objekt verbindet?)
  • Ich will noch mal kurz erzählen, wie z.Z. der Aufbau ist:
    Meine Dokumentenklasse hat eine NSDictionary als Member (nennen wir es mal "content"). Das Dokument macht Änderungen an "content".

    Ziel ist den Inhalt von "content" (NSDictionary) in einem NSTableView darzustellen.
    File's owner von dem Nib, in dem sich der besagte NSArrayController befindet, ist eine Ableitung von NSWindowController.
    Der NSArrayController muss sich also mit dem Model Key Path "document.content.allValues" vom WindowController binden ("document.content" geht ja nicht weil er ein NSArray benötigt).

    Wenn nun das Dokument Änderungen an "content" macht, muss es, wie MAX geschreiben hat, dass mit [self willChangeValueForKey: @"content"] und [self didlChangeValueForKey: @"content"] klammern.

    Aber doch mit @"content" als key, oder?

    Bekommt der NSArrayController eine derartige Änderung per KVO mit?
  • Original von chacko
    Aber doch mit @"content" als key, oder?


    drum sagte ich ja "sowas" und nicht "das da" ;)

    Bekommt der NSArrayController eine derartige Änderung per KVO mit?


    Klar, der ist doch schlau. Und im Zweifel: Probieren geht über studieren :)

    Max
  • Gar kein PProblem.

    Also, du willst in das Dict einen neuen Key einfügen? Mach es einfach. Danach hast du zwei Möglichkeiten:

    Quellcode

    1. - (void)addKey:(NSString*)newKey andObject:(id)obj { // oder wie das bei dir halt gerade heißt
    2. [[self myDict] addObject:obj forKey:key];
    3. [self setContentArray:[[self myDict] allValues]]; // hier wird KVO ausgelöst.
    4. }


    Natürlich kannst du das auch wie Max sagt machen mit Notifications. Kein Problem, macht keinen Unterschied.

    Eleganter als diese "brutalo" Möglichkeiten ist es allerdings, das dezidiert zu machen:

    Quellcode

    1. - (void)addKey:(NSString*)newKey andObject:(id)obj { // oder wie das bei dir halt gerade heißt
    2. [[self myDict] addObject:obj forKey:key];
    3. [self
    4. willChange:NSKeyValueChangeInsertion
    5. valuesAtIndexes:[NSIndexSet indexSetWithIndex:[[self myDict] count]]
    6. forKey:@"contentArray"]
    7. [self setContentArray:[[self myDict] allValues]]; // hier wird KVO ausgelöst.
    8. [self
    9. didChange:NSKeyValueChangeInsertion
    10. valuesAtIndexes:[NSIndexSet indexSetWithIndex:[[self myDict] count]-1]
    11. forKey:@"contentArray"]
    12. }
    Alles anzeigen


    War jetzt aus dem Kopf, also bitte Typos berücksichtigen.
    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"?
  • @MAX: Geht so leider nicht.
    Ein ArrayController lässt sich irgendwie nicht an "allValues" eines NSDictionary binden. Bei mir ging es nur, weil ich einen eigenen NSArrayController geschrieben habe, der in "arrangedObjects" einfach [[document content] allValues" zurückgegeben hat.

    @Tom: Ich hoffe immer noch, ohne eine zusätzliches Array neben dem Dictionary auszukommen.


    Mir kommt das Problem grundsätzlich vor.

    Man kann sich die Aufgabe so eines NSArrayControllers wie die eines NSValueTransformers oder NSFormatters vorstellen:
    Es kommt ein bestimmtes Datenobjekt hinein (in meinem Fall ein NSDictionary) und spuckt die Daten aus dem Eingabe-Datenobjekt in anderen Form aus (hier als NSArray).
    Genauso wie eine ValueTransformer z.B. aus einer Zahl ein Datum oder auch aus einem Array eine Zahl (z.B. durch Bildung der Summe der Zahlen im Array) machen könnte.

    Aber der NSArrayController unterstützt dies nicht so gut, denn er erwartet, dass das Eingabe-Datenobjekt vom selben Typ ist wie sein Ausgabe-Datenobjekt: ein Array.
    Zwar ist der NSArrayController fürs Ausspucken von NSArrays konzipiert (was er ja auch sehr schön macht;-), aber bei den Eingabedaten könnte er etwas flexibler sein.

    Wie könnte man den NSArrayController so "missbrauchen", dass auch andere Eingabe-Typen als Arrays möglich sind?
    Im Prinzip habe ich das ja gemacht: Wie oben beschrieben, einfach "arrangedObjects" in meiner NSArrayController-Ableitung implementiert:

    Quellcode

    1. - (id) arrangedObjects
    2. {
    3. return [[[self document] content] allValues];
    4. }

    Um es ein wenig hübscher zu machen, müsste man im IB das Binding "contentArray" vom meiner NSArrayController-Ableitung auf "document.content" (diese gibt das NSDictionary an) setzten.
    Aber kann ich in der Implementierung "arrangedObjects" das Objekt abfragen, dass über "contentArray" gebunden ist?
    Schön wäre es so (PSEUDO-CODE!):

    Quellcode

    1. - (id) arrangedObjects
    2. {
    3. NSDictionary* docContent = [self objectForBinding: @"contentArray"];
    4. return [docContent allValues];
    5. }

    Der ArrayController würde dann auch mittels KVO Änderungen am Dictionary mitbekommen und könnte ein gecachtes Array erneuern/invalidieren:

    Quellcode

    1. - (void)observeValueForKeyPath: (NSString*)keyPath
    2. ofObject: (id)object
    3. change: (NSDictionary*)change
    4. context: (void*)context
    5. {
    6. NSDictionary* docContent = [self objectForBinding: @"contentArray"];
    7. if ( object == docContent )
    8. [self rearrangeObjects];
    9. }
    10. - (void) rearrangeObjects
    11. {
    12. [_arrangedObject release];
    13. _arrangedObjects = nil;
    14. }
    15. - (id) arrangedObjects
    16. {
    17. if ( _arrangedObjects == nil )
    18. {
    19. NSDictionary* docContent = [self objectForBinding: @"contentArray"];
    20. _arrangedObjects = [[docContent allValues] retain];
    21. }
    22. return _arrangedObjects;
    23. }
    Alles anzeigen

    Ich glaube, hier ist noch mal unser Binding-Spezi Tom gefragt ;)
  • @Tom: Ich hoffe immer noch, ohne eine zusätzliches Array neben dem Dictionary auszukommen.

    Bedenke bitte, dass OO von Beziehungen lebt, nicht von Kopien. Ein zusätzliches Array führt nicht dazu, dass die Objekte im Speicher doppelt liegen. Es führt zu einem zusätzlichen Array, nicht mehr und nicht weniger -- also ziemlich wenig.

    Es ist daher nichts "Böses" ganz viele Container auf eine Collection zu haben. Das verbraucht so gut wie keinen Speicher.

    Es kommt ein bestimmtes Datenobjekt hinein (in meinem Fall ein NSDictionary) und spuckt die Daten aus dem Eingabe-Datenobjekt in anderen Form aus (hier als NSArray).

    Nein, ein Controller hält eben gerade gar keine Daten. Er referenziert Daten. Gehalten werden sie in dem Model.

    Ein ValueTransformer ist nun wieder gänzlich anders aufgebaut. Der Controller referenziert sene Daten. Der Transformer referenziert nichts, sondern übersetzt sie. Das hat nichts miteinander zu tun -- völlig veschiedene Dinge.

    Wie könnte man den NSArrayController so "missbrauchen", dass auch andere Eingabe-Typen als Arrays möglich sind?

    Man sollte gar nichts missbrauchen. Wenn du auf eine lange Zusammenabreit mit Cocoa hoffst, solltest du es so nehmen, wie es ist und Missbrauchsfälle reduzieren.

    Es gibt gar kein Problem, ein ObjectController zu nehmen. Es gibt kein Problem, mehrere Schlüssel anzulegen. Wieso gehst du nicht diese Wege, die dir CB, KVO, KVC weisen, sondern willst etwas missbrauchen?

    Der ArrayController würde dann auch mittels KVO Änderungen am Dictionary mitbekommen und könnte ein gecachtes Array erneuern/invalidieren:

    Das wird schwierig. Das KVO setzt an dem Message-Piping der Setter an. Wenn du einen neuen Key anlegst, hast du definitiv keinen Setter dafür, es sei denn, du programmierst dir den zur Laufzeit.

    Dicts bieten virtuelle Setter, da könntest du dich theoretisch hereinhängen, indem du setObject:forKey überschreibst. Dort kannst du dann eine Notificvation schicken.

    Aber du entfernst dich immer weiter von dem, wie es in Cocoa vorgesehen ist. Und da stellt sich mir die Frage: What the fuck why? Du brauchst es doch gar nicht!
    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"?
  • Bedenke bitte, dass OO von Beziehungen lebt, nicht von Kopien. Ein zusätzliches Array führt nicht dazu, dass die Objekte im Speicher doppelt liegen. Es führt zu einem zusätzlichen Array, nicht mehr und nicht weniger -- also ziemlich wenig.

    Es ist daher nichts "Böses" ganz viele Container auf eine Collection zu haben. Das verbraucht so gut wie keinen Speicher.

    Ist schon klar, aber in meinem Fall würde ich das Array im Model nur hinzufügen, um den NSArrayController zu unterstützen.
    Je mehr Variablen, desto mehr mögliche Zustände, desto mehr Fehlerquellen (z.B. durch das Konsistenthalten der verschiedenen Collections).
    Nein, ein Controller hält eben gerade gar keine Daten. Er referenziert Daten. Gehalten werden sie in dem Model.

    Ein ValueTransformer ist nun wieder gänzlich anders aufgebaut. Der Controller referenziert sene Daten. Der Transformer referenziert nichts, sondern übersetzt sie. Das hat nichts miteinander zu tun -- völlig veschiedene Dinge.

    Das hängt von der Perspektive ab: Auch der NSArrayController muss keine Daten halten, sondern könnte das Array bei jedem Aufruf von"arrangedObjects" erzeugen (zustandslos).
    Ich schreibe hier bewusst "Daten halten", da der NSArrayController auf Basis der Daten im Model ein Array von neuen Datenobjekte aufbauen und nicht nur diese gefiltert oder in neuer Sortierung referenzieren/weiterleiten könnte.
    Aus Sicht des Benutzers (des NSTableViews) vom ArrayController ist ein evtuelles Caching zur Performancesteigerung transparent (OO: Daten sind privat, nur Schnittstellen sind sichtbar).
    Insofern bin ich schon der Meinung, dass der NSArrayController den NSValueTransformern und NSFormattern in seiner Aufgabe und seinem Verhalten ähnelt.

    Ich glaube fast, dass wir Beiden uns nicht richtig verstehen und aneinander vorbeischreiben. Vorschlag: Ich baue ein kleines Testprojekt mit einem NSDictionary, das per NSArrayController in einem NSTableView dargestellt wird.

    Dazu noch eine konkrete Frage: Wie kann ein Observer herausbekommen, zu welchem Objekt er mittels eines exposed bindings (z.B. "contentArray" eines NSArrayControllers) gebunden ist (also zu welchem Array im Model eine NSArrayController gebunden ist)? Ich suche sowas wie [self objectForBinding: @"contentArray"] (s. Code oben).
  • Je mehr Variablen, desto mehr mögliche Zustände, desto mehr Fehlerquellen (z.B. durch das Konsistenthalten der verschiedenen Collections).

    Stimmt schon, wobei die Gefahr bei der Verwendung von Settern klein ist.

    Mir scheint es auch deutlich fehleranfälliger zu sein, das halbe Bindings-System abzuleiten.

    Zu Controlern/Transformern usw.
    Nein, die ähneln sich nicht. Der Transformer kontrolliert nichts. Controller kontrollieren. Das arrangedObjects ist eine klitzekleine Aufgabe der Controller.

    Dazu noch eine konkrete Frage: Wie kann ein Observer herausbekommen, zu welchem Objekt er mittels eines exposed bindings (z.B. "contentArray" eines NSArrayControllers) gebunden ist (also zu welchem Array im Model eine NSArrayController gebunden ist)? Ich suche sowas wie [self objectForBinding: @"contentArray"] (s. Code oben).

    Gar nicht, muss man auch nicht. Du überschreibst dir die bind:toObject:withKeyPath:options:- und unbind:-Methode und merkst es dir dort einfach. Ein Beispiel findet sich in den Graphic Bindings. BTW: Üblicherweise setzen die Bindings keinen retain auf das gebundene Objekt. Kan man natürlich anders machen.
    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"?
  • Hier ein kleines Testprojekt.

    Der Controller bietet die Möglichkeit, ein Collection-Object, das "allValues", "allObjects" oder "objectEnumerator" implementiert, in einem NSTableView darzustellen.

    Z.Z. kann das Collection-Objekt nicht mittels des Controllers geändert werden (wohl aber werden Änderungen an dem Collection-Objekt an das angeschlossene NSTableView propagiert).

    In meinem Anwendungsfall (woraufhin ich diesen Thread gestartet hatte) wird die darzustellende Collection auch nicht direkt vom Benutzer editiert; dies geschieht über Seiteneffekte von anderen Aktionen.

    Würde mich freuen, wenn ihr euch den Code 'mal anschaut und eure Kommentare dazu abgebt (duck!).

    Gruß,
    Tjark