IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

VB.NET Discussion :

Rafraichissement DataGridView temps réel


Sujet :

VB.NET

  1. #1
    Membre averti
    Homme Profil pro
    Inscrit en
    Octobre 2007
    Messages
    31
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 51
    Localisation : France

    Informations forums :
    Inscription : Octobre 2007
    Messages : 31
    Par défaut Rafraichissement DataGridView temps réel
    Bonjour à tous,

    Contexte :
    dév en VB.net et serveur BDD Postgresql en LAN.

    J'ai une liste d'enregistrements, sur une Datagridview, qui doit afficher des données toujours à jour (30 utilisateurs simultanés qui peuvent intervenir sur chaque enregistrement).

    Les utilisateurs ouvrent la liste en début de journée, traitent les interventions une à une jusqu'à ce que toutes soient clôturées.
    (traiter une intervention correspond à double cliquer sur une ligne de la datagrid qui ouvre un form qui affiche le détail de l'intervention.)


    Comment feriez-vous pour que cette datagridview affiche des données toujours à jour et sans casser les éventuels filtres ou tris qui seraient mis en place sur la datagrid ?


    J'espère avoir été clair,
    Merci pour votre aide
    Yo

  2. #2
    Rédacteur/Modérateur


    Homme Profil pro
    Développeur .NET
    Inscrit en
    Février 2004
    Messages
    19 875
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 44
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Développeur .NET
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2004
    Messages : 19 875
    Par défaut
    Si tu utilises un DataSet, il suffit de refaire un Fill sur la table concernée ; ça rafraichit le contenu, et si le DataGridView est lié au DataSet, il sera aussi rafraichi. Les filtres et tris en place ne changent pas.

  3. #3
    Membre averti
    Homme Profil pro
    Inscrit en
    Octobre 2007
    Messages
    31
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 51
    Localisation : France

    Informations forums :
    Inscription : Octobre 2007
    Messages : 31
    Par défaut
    Merci pour ta réactivité Thomas

    C'est effectivement un dataset mais la requête est assez lourde et met plus de 10 secondes à afficher les données (14 800 lignes).
    Le fait de relancer un Fill ne sera pas confortable pour les utilisateurs.

    En gros, les utilisateurs doivent avoir une liste d'interventions qui évolue au fil de la journée. Si un utilisateur met à jour un enregistrement, j'aimerais que la ou les cellules concernées se mettent à jour dans la liste des autres utilisateurs sans intervention humaine.
    Un timer serait approprié mais je ne veux pas que la liste se réinitialise pendant que les utilisateurs travaillent dessus.

    J'essaie de réduire le jeu de données mais le problème de rafraichissement resterait le même.

    Peut-être que ma méthode n'est pas la bonne, comment procèderais-tu ?

    Merci pour ton aide

  4. #4
    Expert éminent Avatar de Pol63
    Homme Profil pro
    .NET / SQL SERVER
    Inscrit en
    Avril 2007
    Messages
    14 204
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Puy de Dôme (Auvergne)

    Informations professionnelles :
    Activité : .NET / SQL SERVER

    Informations forums :
    Inscription : Avril 2007
    Messages : 14 204
    Par défaut
    à la base il n'y a que 2 solutions, qui peuvent se concevoir de manières différentes

    la 1ère c'est de réexécuter la requete entière
    la 2ème c'est d'être averti d'un changement

    réexécuter la requete entière, ca peut se faire de manière intelligente sur un autre thread et ensuite ne modifier que les cellules du datagridview correspondantes
    néanmoins même si c'est une solution à peu près simple c'est toujours idiot de tabasser 30x 15k lignes pour trouver un éventuel changement d'une case

    pour être averti du changement il y a des tas de solutions, certaines sont à penser avant le développement
    - certains sgbdr comme sql server (payant seulement) peuvent faire de la notification de requete, à savoir suite à un executereader avoir un évènement de levé côté vb si les données ont changé
    ca reste très moyennement performant, et ca ne vous dit pas ce qui a changé, il faut quand même relire tout
    - une autre solution est de faire une table qui contient les lignes mises à jour, avec par exemple un trigger sur la table à vérifier qui fait des inserts sur cette table avec les lignes mises à jours (id et type de maj par exemple, avec une info de version ou de client ayant lu l'info)
    ensuite il suffit de faire un thread qui vient lire cette table (donc quelques lignes, puis va lire dans la vraie table pour les ajouts/modifications là aussi quelques lignes avec where sur l'id) et répercutes les actions sur le dgv (ajout/modification/suppression)
    - la meilleure solution reste le serveur, un exe (ou webservice iis) qui est le seul à pouvoir accéder à cette table, et les clients s'y connectent pour obtenir la liste, si un client ajout/modifier/supprime il ne le fait pas sur la table, mais demande au serveur de le faire
    le serveur le fait puis vu qu'il a une liste des clients connectés, il leur envoie la modification
    avec wcf ca peut se faire assez facilement (en .exe ou via iis)
    Cours complets, tutos et autres FAQ ici : C# - VB.NET

  5. #5
    Membre averti
    Homme Profil pro
    Inscrit en
    Octobre 2007
    Messages
    31
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 51
    Localisation : France

    Informations forums :
    Inscription : Octobre 2007
    Messages : 31
    Par défaut
    Parfait, merci beaucoup pour vos réponses

    La solution du serveur se recoupe avec ce que je craignais devoir faire
    Mais il me semble que c'est effectivement la plus adaptée (pour le moment )

    Reste à voir si le serveur intermédiaire ne va pas trop me pénaliser en terme de performances.

    Je laisse la discussion ouverte quelques jours au cas où une personne aurait une autre piste.

    Encore Merci
    Yo

  6. #6
    Expert éminent Avatar de Pol63
    Homme Profil pro
    .NET / SQL SERVER
    Inscrit en
    Avril 2007
    Messages
    14 204
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Puy de Dôme (Auvergne)

    Informations professionnelles :
    Activité : .NET / SQL SERVER

    Informations forums :
    Inscription : Avril 2007
    Messages : 14 204
    Par défaut
    Citation Envoyé par yoyogott Voir le message
    Reste à voir si le serveur intermédiaire ne va pas trop me pénaliser en terme de performances.
    au contraire je pense
    15k lignes, avec 2 dates, 2 entiers et un string de 200 chars ca fait dans les 3 Mo, ton serveur peut garder toute la table en RAM pour ne pas refaire de select en permanence
    Cours complets, tutos et autres FAQ ici : C# - VB.NET

  7. #7
    Membre averti
    Homme Profil pro
    Inscrit en
    Octobre 2007
    Messages
    31
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 51
    Localisation : France

    Informations forums :
    Inscription : Octobre 2007
    Messages : 31
    Par défaut
    Merci pour ta réponse Sébastien,

    Postgres évalue la taille de ma table à 71Mo.

    Peux tu m'en dire un peu sur le principe ?
    Il faut utiliser des sockets pour dialoguer avec le serveur ?

  8. #8
    Rédacteur/Modérateur


    Homme Profil pro
    Développeur .NET
    Inscrit en
    Février 2004
    Messages
    19 875
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 44
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Développeur .NET
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2004
    Messages : 19 875
    Par défaut
    A mon avis, la solution la plus simple pour pas avoir à chambouler complètement l'architecture, c'est d'avoir une colonne date dans ta table, mise à jour à chaque fois qu'un enregistrement est ajouté ou modifié, et de requêter régulièrement (dans un autre thread) par rapport à cette date pour savoir ce qui a changé (ça suppose de mettre un index sur cette colonne). Ca rejoint un peu ce que proposait Sébastien, mais sans utiliser une table séparée.

    Evidemment c'est pas la solution idéale, mais ça a le mérite de s'intégrer facilement à l'existant...

  9. #9
    Expert éminent Avatar de Pol63
    Homme Profil pro
    .NET / SQL SERVER
    Inscrit en
    Avril 2007
    Messages
    14 204
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Puy de Dôme (Auvergne)

    Informations professionnelles :
    Activité : .NET / SQL SERVER

    Informations forums :
    Inscription : Avril 2007
    Messages : 14 204
    Par défaut
    Citation Envoyé par tomlev Voir le message
    A mon avis, la solution la plus simple pour pas avoir à chambouler complètement l'architecture, c'est d'avoir une colonne date dans ta table, mise à jour à chaque fois qu'un enregistrement est ajouté ou modifié, et de requêter régulièrement (dans un autre thread) par rapport à cette date pour savoir ce qui a changé (ça suppose de mettre un index sur cette colonne). Ca rejoint un peu ce que proposait Sébastien, mais sans utiliser une table séparée.

    Evidemment c'est pas la solution idéale, mais ça a le mérite de s'intégrer facilement à l'existant...
    ah oui c'est pas mal aussi, j'y avais pas pensé à celle là
    avec un index sur la date, et une date dans une variable pour faire where date_last_maj > date_last_verif côté programme ; ca reste plus que performant et pas compliqué
    reste à voir comment gérer le delete si nécessaire ...
    Cours complets, tutos et autres FAQ ici : C# - VB.NET

  10. #10
    Membre averti
    Homme Profil pro
    Inscrit en
    Octobre 2007
    Messages
    31
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 51
    Localisation : France

    Informations forums :
    Inscription : Octobre 2007
    Messages : 31
    Par défaut
    Citation Envoyé par Pol63 Voir le message
    ah oui c'est pas mal aussi, j'y avais pas pensé à celle là
    avec un index sur la date, et une date dans une variable pour faire where date_last_maj > date_last_verif côté programme ; ca reste plus que performant et pas compliqué
    reste à voir comment gérer le delete si nécessaire ...

    Ce qui voudrait dire, si j'ai bien compris :
    • J'ajoute une colonne dateMAJ dans la table à contrôler,

    • Dans mon form, je stocke la date qui était dans le champs dateMAJ au dernier contrôle,

    • Régulièrement je SELECT les lignes dont dateMAJ est supérieure à la dernière date que j'avais conservée dans le form (ou une variable)

    • Puis je mets à jour dans mon dataGridView les lignes qui se trouvent dans mon SELECT


    Splendide enfin en théorie
    J'attaque la pratique et reviens faire un rapport

    Merci les gars, j'adore ce site

  11. #11
    Rédacteur/Modérateur


    Homme Profil pro
    Développeur .NET
    Inscrit en
    Février 2004
    Messages
    19 875
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 44
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Développeur .NET
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2004
    Messages : 19 875
    Par défaut
    Citation Envoyé par Pol63 Voir le message
    avec un index sur la date, et une date dans une variable pour faire where date_last_maj > date_last_verif côté programme
    Plutôt que date_last_verif, il faut utiliser la dernière date de mise à jour que que as récupérée ; ça évite de se prendre la tête avec les différences d'heure entre le client et le serveur

    Citation Envoyé par Pol63 Voir le message
    reste à voir comment gérer le delete si nécessaire ...
    Ah oui, il me semblait bien que j'oubliais quelque chose

    Bah là pour le coup il faut une table spécifique, avec Id et date de suppression par exemple. Par contre il faut la nettoyer de temps en temps ; par exemple tu peux faire un process de maintenance côté serveur qui supprime tous les jours les lignes qui correspondent aux suppressions de la veille.

  12. #12
    Membre averti
    Homme Profil pro
    Inscrit en
    Octobre 2007
    Messages
    31
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 51
    Localisation : France

    Informations forums :
    Inscription : Octobre 2007
    Messages : 31
    Par défaut
    Citation Envoyé par tomlev Voir le message
    Plutôt que date_last_verif, il faut utiliser la dernière date de mise à jour que que as récupérée ; ça évite de se prendre la tête avec les différences d'heure entre le client et le serveur



    Ah oui, il me semblait bien que j'oubliais quelque chose

    Bah là pour le coup il faut une table spécifique, avec Id et date de suppression par exemple. Par contre il faut la nettoyer de temps en temps ; par exemple tu peux faire un process de maintenance côté serveur qui supprime tous les jours les lignes qui correspondent aux suppressions de la veille.

    Donc on lance une première requête qui check quelles sont les enregistrements supprimés, on répercute sur la DataGridView (DGV) puis on lance l'opération de mise à jour.
    Dans mon cas, il n'y aura pas de suppression mais pour un cas général ca peut le faire ...

    Il me reste à trouver la syntaxe qui permet de mettre à jour les lignes du DGV qui correspondent aux enregistrements qui ont une dateMAJ > date de dernier check et ca devrait être bon.

    COol

  13. #13
    Expert confirmé Avatar de Graffito
    Profil pro
    Inscrit en
    Janvier 2006
    Messages
    5 993
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Janvier 2006
    Messages : 5 993
    Par défaut
    Bah là pour le coup il faut une table spécifique, avec Id et date de suppression par exemple.
    Pour les delete, l'autre solution est d'ajouter dans la table en plus de la date de dernière modification un champ booléen indiquant la destruction logique :

    • L'instruction DELETE serait remplacée par un UPDATE mettant à jour date et positionnant l'indicateur de suppression à true,
    • Le SELECT du fill() initial excluerait les enregistrements avec indicateur à true,
    • Et périodiquement DELETE des "vieux" enregistrements détruits logiquement.

  14. #14
    Rédacteur/Modérateur


    Homme Profil pro
    Développeur .NET
    Inscrit en
    Février 2004
    Messages
    19 875
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 44
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Développeur .NET
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2004
    Messages : 19 875
    Par défaut
    Citation Envoyé par yoyogott Voir le message
    Il me reste à trouver la syntaxe qui permet de mettre à jour les lignes du DGV qui correspondent aux enregistrements qui ont une dateMAJ > date de dernier check et ca devrait être bon.
    Ne mets pas à jour directement le DGV ; mets à jour la DataTable faisant un Fill avec la requête filtrée par dateMAJ. Ca se répercutera automatiquement sur le DGV lié.

    Citation Envoyé par Graffito Voir le message
    Pour les delete, l'autre solution est d'ajouter dans la table en plus de la date de dernière modification un champ booléen indiquant la destruction logique :

    • L'instruction DELETE serait remplacée par un UPDATE mettant à jour date et positionnant l'indicateur de suppression à true,
    • Le SELECT du fill() initial excluerait les enregistrements avec indicateur à true,
    • Et périodiquement DELETE des "vieux" enregistrements détruits logiquement.
    Oui mais si tu fais ça, ça implique de modifier toutes les requêtes qui tapent sur cette table pour prendre en compte cet indicateur (à moins que toutes les requêtes passent par une vue, auquel cas il suffit de modifier la vue)

  15. #15
    Membre averti
    Homme Profil pro
    Inscrit en
    Octobre 2007
    Messages
    31
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 51
    Localisation : France

    Informations forums :
    Inscription : Octobre 2007
    Messages : 31
    Par défaut
    Citation Envoyé par tomlev Voir le message
    Ne mets pas à jour directement le DGV ; mets à jour la DataTable faisant un Fill avec la requête filtrée par dateMAJ. Ca se répercutera automatiquement sur le DGV lié.
    Si je mets à jour la DataTable en filtrant sur la dateMAJ, il ne restera dans celle-ci que les enregistrements qui ont fait l'objet d'une mise à jour non ?

    Le principe global parait clair, en revanche la mise en place est plus complexe que je ne l'imaginais.

    Actuellement :
    • Je charge, à l'ouverture du form, ma DataGridView (basé sur ds_interventions)
    • Je déclenche le timer qui va se charger de rafraichir les données
    • Le timer récupère les données mises à jour (date_MAJ > LastDateVerif) dans un nouveau dataset (ds_update_data)


    Il me reste à Updater les données de la DataTable (de ds_update_data) pour que la DataGridView rafraichisse les lignes correspondantes sans fausser le filtre actuel

    Je butte sur cette dernière partie pour le moment

  16. #16
    Rédacteur/Modérateur


    Homme Profil pro
    Développeur .NET
    Inscrit en
    Février 2004
    Messages
    19 875
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 44
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Développeur .NET
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2004
    Messages : 19 875
    Par défaut
    Citation Envoyé par yoyogott Voir le message
    Si je mets à jour la DataTable en filtrant sur la dateMAJ, il ne restera dans celle-ci que les enregistrements qui ont fait l'objet d'une mise à jour non ?
    Non, ça récupère les nouveaux enregistrements et ça met à jour ceux qui sont déjà dans la DataTable. Les autres ne sont pas touchés.

    Citation Envoyé par yoyogott Voir le message
    Le timer récupère les données mises à jour (date_MAJ > LastDateVerif) dans un nouveau dataset (ds_update_data)
    Non, récupère les dans la même DataTable

    Citation Envoyé par yoyogott Voir le message
    Il me reste à Updater les données de la DataTable (de ds_update_data)
    Si tu récupères les mises à jour dans la même DataTable, la question ne se pose plus

    Citation Envoyé par yoyogott Voir le message
    pour que la DataGridView rafraichisse les lignes correspondantes sans fausser le filtre actuel
    Comment tu filtres ? en mémoire ou en SQL ? Si tu filtres avec une DataView ou une BindingSource, tu n'as rien à faire. Les lignes mises à jour ne s'afficheront que si elles correspondent au filtre.

  17. #17
    Membre averti
    Homme Profil pro
    Inscrit en
    Octobre 2007
    Messages
    31
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 51
    Localisation : France

    Informations forums :
    Inscription : Octobre 2007
    Messages : 31
    Par défaut
    Très bien, Merci pour ton aide Thomas,
    Je filtre en mémoire, donc tu as répondu à ma question

    Il y a quand même quelque chose qui m'échappe.
    Dans mon DataSet initial, j'ai mes données issues du premier SELECT qui viennent peupler mon DataGridView. Jusque là, pas de problème.

    J'instancie un timer qui déclenche un SELECT des données mises à jour.

    Je n'ai pas la syntaxe pour faire l'UPDATE du DataSet.
    DataAdapter.Fill(dataset_Initial, datatable_concernée) ? ou DataAdapter.Update ?

    Je débute dans les technologies d'accès aux données .Net, désolé pour ces questions qui sont peut être évidentes pour la plupart d'entre vous.

    Encore Merci pour vos interventions

  18. #18
    Rédacteur/Modérateur


    Homme Profil pro
    Développeur .NET
    Inscrit en
    Février 2004
    Messages
    19 875
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 44
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Développeur .NET
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2004
    Messages : 19 875
    Par défaut
    Citation Envoyé par yoyogott Voir le message
    Je n'ai pas la syntaxe pour faire l'UPDATE du DataSet.
    DataAdapter.Fill(dataset_Initial, datatable_concernée) ? ou DataAdapter.Update ?
    Tu veux dire pour rafraichir les données ? C'est toujours Fill
    La méthode Update sert à mettre à jour la DB en fonction des modifications apportées au DataSet

  19. #19
    Membre averti
    Homme Profil pro
    Inscrit en
    Octobre 2007
    Messages
    31
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 51
    Localisation : France

    Informations forums :
    Inscription : Octobre 2007
    Messages : 31
    Par défaut
    Je voulais plutôt dire pour ne récupérer que les lignes dont date_MAJ > dernierDateCheck.

    Après test, le fait de faire un Fill sur le dataset lié à ma DataGridView crée autant de doublons que d'exécution du traitement.
    Le Fill ne fait pas un Update de ma ligne mais un INSERT.

    Si je fais un dataset = nothing avant de lancer le Fill, il sera complètement vidé et ne contiendra après coup que les lignes qui ont été mises à jour ...

    On y est presque, ca doit être un détail ...

    Pour être plus précis, le dataAdapter.SelectCommand pointe sur une Vue de la base de données PostgreSQL.

    Voici un extrait du code (GenericDS est déclaré au niveau global pour être visible dans mon évènement Timer) :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
            Dim ctn As New System.Data.Odbc.OdbcConnection(My.Settings.ConnexionStringServeur014)
            Dim cmd As OdbcCommand
            Dim da As New OdbcDataAdapter
     
            cmd = ctn.CreateCommand
            cmd.CommandText = "SELECT * FROM vue_interventions WHERE date_MAJ > '" & GDernierCheck & "'"
     
            da.SelectCommand = cmd
            da.Fill(GenericDS)
     
            GDernierCheck = Date.Now

  20. #20
    Membre Expert

    Homme Profil pro
    Développeur .NET
    Inscrit en
    Novembre 2010
    Messages
    2 067
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Développeur .NET

    Informations forums :
    Inscription : Novembre 2010
    Messages : 2 067
    Par défaut
    Et pourquoi pas une table qui contient seulement la date de dernière maj et un trigger sur ta table qui a chaque fois qu'il est déclenché met à jour cette date.

+ Répondre à la discussion
Cette discussion est résolue.
Page 1 sur 2 12 DernièreDernière

Discussions similaires

  1. Comment faire du rafraichissement temps réel?
    Par yvon_huynh dans le forum Général JavaScript
    Réponses: 6
    Dernier message: 15/11/2008, 15h47
  2. [MFC] graphique temps réel
    Par _Thomas_ dans le forum MFC
    Réponses: 10
    Dernier message: 01/06/2004, 11h56
  3. Voir requête éxécuté en temps réel ?
    Par [DreaMs] dans le forum MS SQL Server
    Réponses: 2
    Dernier message: 08/01/2004, 14h52
  4. cubes temps réel en ROLAP
    Par Guizz dans le forum MS SQL Server
    Réponses: 4
    Dernier message: 09/07/2003, 16h36
  5. Durée d'un traitement temps réel
    Par Almex dans le forum C
    Réponses: 5
    Dernier message: 29/03/2003, 14h15

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo