Bindings: Moi, pas comprendre

CéroceCéroce Membre, Modérateur
16:56 modifié dans API AppKit #1
Bonjour à  tous,


En tant que développeur qui s'intéresse à  Cocoa depuis quelques années, j'ai été formé à  la vieille école, celle où on concevait ses classes MVC et on reliait tout ce joli monde par des actions/target. Cependant, vient un temps où on voudrait profiter des avantages des bindings, et bien que m'y étant replongé à  plusieurs reprises, je n'ai jamais réussi à  faire fonctionner un semblant de programme correctement.
Mes problèmes viennent de la documentation d'Apple, qui passe son temps à  glorifier les bindings et nous expliquer les moindres détails de leur fonctionnement, mais incapable d'expliquer dans un cas simple quels objets sont vraiment nécessaires, et quels méthodes il faut implémenter.


Voici un exemple simple, il s'agit de dessiner un rectangle dans une vue:
(voir FenetreCarre.png en fichier joint).

Comme vous pouvez le voir, sont prévus de pouvoir modifier l'épaisseur du tracé, et la couleur du rectangle.

Pour l'instant, je m'intéresse uniquement à  l'épaisseur. Vous pouvez voir dans le projet ci-joint que j'ai défini deux classes:
- Une classe modèle, CeRect
J'y ai défini la méthode setWidth:. A priori, je n'étais pas obligé de la définir, mais elle me permet d'afficher les changements de valeurs.

- Une classe vue, CeRectView

Dans le fichier .nib, le NSObjectController gère une instance de CeRect.
Les instances de NSTextField et NSSlider sont bien bindées sur NSObjectController, pour la clé selection.width.

En lançant le programme, on peut constater que le modèle est bien mis à  jour en bougeant le curseur, ou en tapant dans le champ: la fenêtre de log affiche bien des messages.


Commencent maintenant mes difficultés: mettre à  jour ma vue.

1) J'ai ajouté cela à  ma classe CeRectView:
<br />- (void)awakeFromNib<br />{<br />	[controller addObserver:self forKeyPath:@&quot;selection.width&quot; options:0 context:nil];<br />}<br />


Est-ce nécessaire? Cela oblige à  créer une outlet vers le NSObjectController " ce ne me semble pas très binding tout ça...

2) Comment obtenir la nouvelle valeur de width? Voilà  un code qui ne fonctionne pas, en plus de me sembler compliqué pour si peu:
<br />- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context<br />{<br />	<br />	if ([keyPath isEqual:@&quot;selection.width&quot;])<br />	{<br />		_rectWidth = [[change objectForKey:NSKeyValueChangeNewKey] floatValue];<br />		NSLog(@&quot;width = %f&quot;, _rectWidth);<br />		[self setNeedsDisplay:YES];<br />	}<br />}<br />


Pouvez-vous me guider sur ces deux points pour commencer.
Merci d'avance!


Réponses

  • Philippe49Philippe49 Membre
    16:56 modifié #2
    Au moins :

    1) relier l'outlet content de ce que tu appelles  windowcontroller (pourquoi ce nom?) à  la vue CeRectView

    2) Dans l'interface de CeRectView


    @interface CeRectView : NSView
    {
    float width;
    }
    -(float) width;
    - (void)setWidth:(float)width;

    @end


    3) Dans l'implémentation :

    #import "CeRectView.h"

    @implementation CeRectView

    - (id)initWithFrame:(NSRect)frame {
        self = [super initWithFrame:frame];
        if (self)
    {
    width = 1.0;

    /*red = 0.0;
    green = 1.0;
    blue = 0.0;
    alpha = 1.0;*/
        }
        return self;
    }

    - (void)drawRect:(NSRect)rect
    {
    [[NSColor whiteColor]set];
    [NSBezierPath fillRect:rect];

        [[NSColor colorWithCalibratedRed:0.0 green:1.0 blue:0.0 alpha:1.0] set];

    NSBezierPath* path = [NSBezierPath bezierPathWithRect:NSMakeRect(60, 80, 300, 200)];
    [path setLineWidth:width];
    [path stroke];

    }
    -(float) width{return width;}
    - (void)setWidth:(float)aWidth
    {
    width=aWidth;

    [self setNeedsDisplay:YES];
    }
  • Philippe49Philippe49 Membre
    16:56 modifié #3
    Avc ObjectiveC-2.0 , on préfèrera

    @interface CeRectView : NSView
    {
      CGFloat      width;
    }
    @property CGFloat width;

    @end


    et les accesseurs -(float) width et - (void)setWidth:(float)aWidth se réduisent à 
    @synthesize width;

  • laurrislaurris Membre
    16:56 modifié #4
    dans 1209054533:


    <br />- (void)awakeFromNib<br />{<br />	[controller addObserver:self forKeyPath:@&quot;selection.width&quot; options:0 context:nil];<br />}<br />
    


    Est-ce nécessaire? Cela oblige à  créer une outlet vers le NSObjectController " ce ne me semble pas très binding tout ça...


    J'ai l'impression que tu observes le NSObjectController. Normalement, il faut observer l'objet contenu par le NSObjectController. -(void)observe ... sert uniquement à  mettre à  jour la vue quand la propriété de l'objet observé change.
    Dans la doc (ou alors dans google ?) existe un exemple nommé GraphicsBindings qui montre comment faire ça en restant "KVO compliant".
  • Philippe49Philippe49 Membre
    16:56 modifié #5
    dans 1209054533:


    <br />- (void)awakeFromNib<br />{<br />	[controller addObserver:self forKeyPath:@&quot;selection.width&quot; options:0 context:nil];<br />}<br />
    


    Est-ce nécessaire? Cela oblige à  créer une outlet vers le NSObjectController " ce ne me semble pas très binding tout ça...


    Non.

    De toute façons, pour faire ce qui semble être ton idée derrière tout cela (programmer un binding à  la main ?), on utiliserait
    - (void)bind:(NSString *)binding toObject:(id)observableController withKeyPath:(NSString *)keyPath options:(NSDictionary *)options

  • CéroceCéroce Membre, Modérateur
    16:56 modifié #6
    Merci à  tous les deux pour vos réponses.

    Pour commencer, ce que m'a dit Philippe m'a permis de comprendre ce qu'était le "content" du NSObjectController: la vue. Finalement, c'est simple!

    En appelant la clé width à  la fois pour les classes CeRect et CeRectWidth, le programme fonctionne correctement. Je vous le fournis d'ailleurs en pièce jointe.


    Voici donc une nouvelle question: est-on obligé d'appeler les clés de la même manière dans les deux classes?
    Par exemple, si j'appelle la clé rectWidth dans CeRectView et que je définis bien les méthodes -rectWidth et -setRectWidth:, comment dois-je binder dans IB ?

    dans 1209072738:

    De toute façons, pour faire ce qui semble être ton idée derrière tout cela (programmer un binding à  la main ?)


    Non, vraiment, le but c'est seulement de comprendre comment remplacer mes vieux target/actions par les bindings pour gagner du temps, profiter d'AppleScript automatiquement et de CoreData...

    @Laurris: J'ai trouvé l'exemple GraphicsBindings dans le dossier Example, pour un programme Python. Comme le .nib utilisait des NSArrayController, ça sortait du cadre de ce programme qui veut vraiment aller au cas le plus simple (un seul rectangle). Cependant, je pense que ça me sera utile dès que j'aurais deux rectangles  ;)



    Bon, maintenant, j'essaie pour la couleur...
  • Philippe49Philippe49 Membre
    avril 2008 modifié #7
    dans 1209114672:

    Finalement, c'est simple!
    En appelant la clé width à  la fois pour les classes CeRect et CeRectWidth, le programme fonctionne correctement. Je vous le fournis d'ailleurs en pièce jointe.


    C'est encore plus simple :

    1) Ta classe CeRect ne sert à  rien : supprimes-là , tu verras que cela marche pareil.

    2) Il est d'usage d'appeler la variable, l'identificateur du binding et les setter/getter avec le même nom : c'est la "philosophie" générale. Ainsi, tu l'appelles rectWidth (ce qui est sans doute plus prudent, tellement le mot width est surchargé) mais alors le binding dans IB doit être fait avec la clé rectWidth et les setter/getter s'écrivent
    -(void) rectWidth;
    -(float) setRectWidth:(float) aWidth;

    3) Ces setter ne sont alors pas nécessaires car ici ils ne font rien d'autre que ce que fait le Key-Value-Coding avec des instructions du type
    [machin setValue:aWidth forKey:@rectWidth]


    dans 1209114672:

    Voici donc une nouvelle question: est-on obligé d'appeler les clés de la même manière dans les deux classes?

    Non rien n'oblige dans un multiple binding à  ce que la clé ne soit pas la même, ceci dit cela risque d'être  ... :crackboom:-

  • CéroceCéroce Membre, Modérateur
    16:56 modifié #8
    Après avoir réussi à  faire de même pour la couleur, je croyais avoir compris, mais je reviens sur les même problèmes qu'au départ.
    Le programme marche très bien, cependant, je n'arrive pas à  me raccrocher à  ce que je connais: le MVC.

    dans 1209117423:

    C'est encore plus simple :
    1) Ta classe CeRect ne sert à  rien : supprimes-là , tu verras que cela marche pareil.


    J'imagine que tu veux dire: "Elle ne sert à  rien pour faire fonctionner ton programme actuel". Par principe, il faut bien que je conserve ma couche modèle, sinon, je ne peux plus sauvegarder sur le disque.

    Résumons.
    - Pour les vues: la CeRectView, le NSSlider et le NSColorWell
    - Pour le modèle: la CeRect
    - Pour le contrôleur: le NSObjectController (appelé RectController)

    D'après ce que j'ai compris jusqu'à  présent:
    - je dois faire pointer l'outlet content de RectController sur la CeRectView
    - je dois mettre, sous IB, CeRect comme Class Name de RectController -> j'ai cru comprendre que ça lui demandait d'instancier un CeRect.

    Mon problème, c'est que les méthodes de CeRect (init, setWidth:, setColor:) ne sont pas appelées (alors que ça marche dans le premier exemple que j'ai donné). Plus je lis la doc d'Apple, et moins je comprends.

    Merci de m'aider!

  • Philippe49Philippe49 Membre
    16:56 modifié #9
    dans 1209487108:

    J'imagine que tu veux dire: "Elle ne sert à  rien pour faire fonctionner ton programme actuel". Par principe, il faut bien que je conserve ma couche modèle, sinon, je ne peux plus sauvegarder sur le disque.


    Il n'y a rien de miraculeux.
    Un binding , c'est quoi : en gros la mise en place de notifications lors de changements pour certains mots-clés.
    Le RectController sert de proxy, d'intermémédaire, dans IB entre l'instance CeRectView et les autres objets de l'interface graphique. (il est d'ailleurs superflu : un binding par programmation serait tout aussi efficace)
    Lorsqu'on change le slider, une notification est envoyée au RectController qui la transmets à  son content, et au textField.
    Un point, c'est tout.

    Le dessin du rectangle n'existe que par l'instruction
    NSBezierPath* path = [NSBezierPath bezierPathWithRect:NSMakeRect(60, 80, 300, 200)];
    que tu as mis dans la méthode de drawRect et aucun CeRect n'est créé dans ton programme.


    dans 1209487108:

    Mon problème, c'est que les méthodes de CeRect (init, setWidth:, setColor:) ne sont pas appelées ]


    Non seulement elles ne sont pas appelées mais aucun CeRect n'est créé : la tâche déclarée du RectController est le rôle d'intermédiaire, de proxy entre le NSSlider, le NSTextField et le CeRectView. Aucun CeRect là -dedans.

    dans 1209487108:

    D'après ce que j'ai compris jusqu'à  présent:
    - ...
    - je dois mettre, sous IB, CeRect comme Class Name de RectController -> j'ai cru comprendre que ça lui demandait d'instancier un CeRect.

    Si
    1) l'IBOutlet  "content" n'était pas connecté
    2) la classe du RectController était déclaré comme un CeRect
    3) La case "automatically prepares content" était coché

    alors une instance CeRect serait créée, mais elle n'aurait rien à  voir avec CeRectView.

    Bref, il ne faut pas demander aux bindings de tout faire, simplement, cela permet de notifier automatiquement des changements dans les valeurs de certaines variables.
    Ces notifications sont parfois des créations d'instances (controller pour collections : array, dictionaries, tree)
     
  • Philippe49Philippe49 Membre
    avril 2008 modifié #10
    dans 1209487108:

    D'après ce que j'ai compris jusqu'à  présent:
    - ...
    - je dois mettre, sous IB, CeRect comme Class Name de RectController -> j'ai cru comprendre que ça lui demandait d'instancier un CeRect.


    Dans la CLass Name, il n'y a rien à  mettre si le content est connecté par Outlet, sinon il faudrait mettre CeRectView
  • Philippe49Philippe49 Membre
    mai 2008 modifié #11
    dans 1209487108:

    J'imagine que tu veux dire: "Elle ne sert à  rien pour faire fonctionner ton programme actuel". Par principe, il faut bien que je conserve ma couche modèle, sinon, je ne peux plus sauvegarder sur le disque.


    Ce n'est pas un ObjectController qui va archiver quoi que ce soit.
    Ou alors il faudrait sous-classer NSObjectController, mais quel intérêt par rapport à  le faire dans un contrôleur général de l'appli ?
  • CéroceCéroce Membre, Modérateur
    16:56 modifié #12
    Je remonte ce message pour coucher tout ce que j'ai fini par comprendre et qui risque d'être utile à  d'autres:

    • Comme je le supposais au début, le Content object correspond bien au modèle dans le paradigme MVC. Dans mon exemple, il faut:
    - laisser l'outlet content du NSObjectController détachée.
    - mettre Class Name à  CeRect qui est la classe du modèle
    Il ne faut donc pas relier la vue (CeRectView) à  l'outlet content, comme cela était recommandé plus haut. Certes, cela permet de binder la vue au contrôleur, mais du coup, le modèle n'est pas mis à  jour, ce qui s'avère ennuyeux pour faire un programme utile.


    • Le problème provient du fait que les vues persos (Custom views) ne publient pas leurs bindings. On ne peut donc pas les binder dans IB, sauf à  créer une palette IB, comme expliqué ici: http://www.mactech.com/articles/mactech/Vol.21/21.10/Palettes/index.html.

    • Cette méthode est toutefois assez complexe à  mettre en oe“uvre, d'autant plus que la plupart des Custom Views ne sont utilisées qu'à  un seul endroit dans une appli. D'après ce que j'ai vu, l'autre solution consiste donc à  tirer un outlet depuis un autre contrôleur vers la vue. Dans ce contrôleur, on bindera la vue à  la mano après le chargement du nib. C'est la méthode adoptée dans l'exemple évoqué par Laurris, GraphicsBindings, que vous trouverez ici: http://homepage.mac.com/mmalc/CocoaExamples/GraphicsBindings.zip
    Vous verrez que la classe MyDocument possède des outlets vers les vues personnalisées et les binde dans la méthode windowControllerDidLoadNib:.

    Un autre exemple intéressant, mais insuffisamment documenté: http://developer.apple.com/samplecode/BindingsJoystick/BindingsJoystick.zip
    Cet exemple correspond à  l'introduction donnée par Apple: http://developer.apple.com/documentation/Cocoa/Conceptual/CocoaBindings/Concepts/HowDoBindingsWork.html#//apple_ref/doc/uid/20002373


    Désolé de ne pas avoir fait de programme d'exemple, mais vous devriez gagner du temps avec tout ça.
  • Philippe49Philippe49 Membre
    16:56 modifié #13
    dans 1210611529:

    les IBPalettes ne sont plus à  l'ordre du jour.
    Il s'agit maintenant d'IBPlugin et la technologie a bien évoluée.

    dans 1210611529:

    D'après ce que j'ai vu, l'autre solution consiste donc à  tirer un outlet depuis un autre contrôleur vers la vue. Dans ce contrôleur, on bindera la vue à  la mano après le chargement du nib ...

    Ben, c'est exactement la méthode proposée ci-dessus.
    Et si on veut que le rectangle dessiné soit géré en binding, il faut faire un binding par keyPath comme indiqué ici



    dans 1210611529:

    Désolé de ne pas avoir fait de programme d'exemple, mais vous devriez gagner du temps avec tout ça.
  • CéroceCéroce Membre, Modérateur
    16:56 modifié #14
    Comme je suis reparti sur les bindings, je vous donne cette fois-ci un programme d'exemple d'utilisation des bindings avec une Custom View (ce qui est compliqué, cf. les messages précédents).
    J'ai essayé de faire le plus simple possible, tout en faisant quelque chose qui respecte le paradigme MVC. Le programme ne fait qu'afficher une vue toute bleue dont l'opacité est réglable par un curseur ou un champ éditable.

    - Le modèle correspond à  CeSimplestModel. Il ne possède qu'un attribut, l'opacité.
    - La custom view est CeSimplestCustomView. Comme indiqué dans les docs d'Apple, il faut implémenter les méthodes - bind:toObject:withKeyPath:options: et - observeValueForKeyPath:ofObject:change:context: pour que ça marche. Je vous laisse voir le code.
    - Comme dit dans un message précédent, le problème est qu'on ne peut pas binder la vue sous IB. Pour contourner cela, j'ai créé une classe CeBinder, dont la méthode -awakeFromNib binde la vue avec le NSObjectController. Deux outlets sont tirées dans IB pour cela.

Connectez-vous ou Inscrivez-vous pour répondre.