Renseigner les champs d'une forme avec des requêtes asynchrones

MGD Software Messages postés 186 Date d'inscription vendredi 1 septembre 2006 Statut Membre Dernière intervention 23 avril 2022 - Modifié le 10 déc. 2020 à 19:11
Whismeril Messages postés 19029 Date d'inscription mardi 11 mars 2003 Statut Contributeur Dernière intervention 26 avril 2024 - 12 déc. 2020 à 18:19
Bonjour,

Je sais, le problème est très connu, seulement voilà, je n'ai trouvé aucune réponse claire à mon problème, même en écumant le net. On trouve beaucoup de choses sur les méthodes asynchrone, mais je n'ai pas trouvé ou pas compris comment on en "sortait".

Voici mon problème (mais je souhaiterait une réponse générique):
J'ai un forme dans laquelle figure un DataGridView dont plusieurs champs doivent être remplis par une requête internet.
Cela fonctionne très bien avec des requêtes synchrones, mais quand il y a beaucoup de lignes cela prend un temps fou, d'autant qu'une des colonnes du DataGridView est remplie avec des images issues du web.

Le problème est que lorsque j'utilise une tâche (Task), je n'ai pas accès au thread appelant et donc je ne peux pas remplir ma grille. J'ai tenté un invoke + delegate, mais ça ne fonctionne pas, le "await" n'est pas accepté. En voici cependant le code pour montrer ce que je voudrais faire :
// Remplissage des images en asynchrone
this.Invoke(new MethodInvoker(delegate
{
    foreach (DataGridViewRow Row in dgvList.Rows)
    {
        FilmInfo Film = Liste[Row.Index];
        if (Film.poster_path != null && !Film.poster_path.Equals(""))
        {
            DataGridViewImageCell ImgCell = (DataGridViewImageCell)Row.Cells["Affiche"];
            HttpWebRequest Request = (HttpWebRequest)WebRequest.Create(Film.poster_path);
            Request.Method = "GET";
            HttpWebResponse Response = (HttpWebResponse)(await Request.GetResponseAsync());
            if (Response != null)
            {
                Bitmap bmp = new Bitmap(Response.GetResponseStream());
                Response.Close();
                Film.poster = bmp;
                // Redimensionnement de l'image à la largeur de la colonne
                int PosterWidth = dgvList.Columns["Affiche"].Width;
                int PosterHeight = PosterWidth * bmp.Height / bmp.Width;
                Size NewSize = new Size(PosterWidth, PosterHeight);
                ImgCell.Value = new Bitmap(bmp, NewSize);
            }
        }
    }
}));

Film est une classe, le membre poster_path contient l'URL de l'image à récupérer et le membre poster est une variable image destinée à contenir l'affiche du film.

Quelqu'un peut-il m'indiquer une routine simple (?!) permettant de mettre à jour un champ d'une forme à partir d'une requête asynchrone, ou de renseigner une variable extérieure à la tâche ?

Merci

Edit : en enlevant le "await", en utilisant GetResponse à la place de GetResponseAsync , et en mettant une ligne dgvList.Refresh() en fin de boucle, cela semble fonctionner, les images s'affichent après le reste du tableau. Mais je ne suis pas sur que ce soit la bonne solution...

4 réponses

Whismeril Messages postés 19029 Date d'inscription mardi 11 mars 2003 Statut Contributeur Dernière intervention 26 avril 2024 656
10 déc. 2020 à 20:23
Hello,

je ne saurais pas répondre à ta question, mais si ça marche, c'est déjà une bonne solution. Non?
0
MGD Software Messages postés 186 Date d'inscription vendredi 1 septembre 2006 Statut Membre Dernière intervention 23 avril 2022 2
11 déc. 2020 à 10:13
Bonjour Whismeril,

En fait, cela ne fonctionne pas si bien que ça. Si c'est moins frustrant pour l'utilisateur de ne pas rester longtemps sur une grille vide, il n'en reste pas moins que le programme ne rend pas la main avant que toutes les images ne soient chargées. On ne peut donc pas faire défiler la grille, ni faire quoi que ce soit pendant un bon moment.

J'ai essayé d’utiliser le backgroundworker, mais tout ce qu'il sait faire en matière de communication avec les autres threads c'est d'envoyer deux évènements, un de pourcentage d'exécution et l'autre de fin de process. Ce n'est pas utilisable dans mon cas.

Il doit bien y avoir un moyen de faire communiquer deux threads en échangeant un objet commun, bon sang !

Une solution provisoire est de placer un Application.DoEvents() dans la boucle, mais alors là ce n'est VRAIMENT PAS la bonne solution.

0
Whismeril Messages postés 19029 Date d'inscription mardi 11 mars 2003 Statut Contributeur Dernière intervention 26 avril 2024 656
11 déc. 2020 à 14:32
Salut, tu n’es pas limité à un pourcentage avec le backgroundworker.

Mais quelque soit le type de thread, il y a plusieurs façons de faire.

Je tache de te faire un exemple dans le week-end.
0
Whismeril Messages postés 19029 Date d'inscription mardi 11 mars 2003 Statut Contributeur Dernière intervention 26 avril 2024 656
12 déc. 2020 à 18:19
Bonsoir

concernant le backgroundworker.
La méthode ReportProgress dispose de 2 surcharges, la seconde te permet de passer l'argument UserState de type object (donc n'importe quoi)



Que tu récupères dans le paramètre e de la méthode abonnée

Il te faut juste le caster comme il faut pour t'en servir.

Mais je vais te montrer une autre méthode.
Comme je ne sais pas ce que tu vas chercher comme image, j'en ai pris une dans "Mes Images"

J'ai d'abord écrit une classe.
Elle implémente InotifyPropertyChanged pour le binding.
using System.ComponentModel;
using System.Drawing;

namespace test
{
    class TestMGD : INotifyPropertyChanged
    {
        private Bitmap image;
        /// <summary>
        /// Image
        /// </summary>
        public Bitmap Image
        {
            get { return image; }
            set
            {
                if (image != value)
                {
                    image = value;
                    GenerePropertyChanged("Image");
                }
            }
        }

        private int id;
        /// <summary>
        /// ID
        /// </summary>
        public int ID
        {
            get { return id; }
            set
            {
                if (id != value)
                {
                    id = value;
                    GenerePropertyChanged("ID");
                }
            }
        }

        private string nom;
        /// <summary>
        /// Nom
        /// </summary>
        public string Nom
        {
            get { return nom; }
            set
            {
                if (nom != value)
                {
                    nom = value;
                    GenerePropertyChanged("Nom");
                }
            }
        }

        private string cheminImage;
        /// <summary>
        /// Chemin de l'image
        /// </summary>
        public string CheminImage
        {
            get { return cheminImage; }
            set
            {
                if (cheminImage != value)
                {
                    cheminImage = value;
                    GenerePropertyChanged("CheminImage");
                }
            }
        }


        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;

        private void GenerePropertyChanged(string Propriete)
        {
            if (this.PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(Propriete));
        }

        #endregion

    }
}


Je binde cette classe au datagridview comme décrit dans mon tuto https://codes-sources.commentcamarche.net/faq/1291-utilisation-du-binding-au-travers-de-l-objet-databindingsource

Et voici le code de la form
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using System.IO;
using System.Threading;

namespace test
{
    public partial class Form3 : Form
    {

        public Form3()
        {
            InitializeComponent();
        }

        List<TestMGD> lesItemsDeMDG;
        BackgroundWorker bgw;

        private void Form3_Load(object sender, EventArgs e)
        {
            //initialisation des la collection source de données
            lesItemsDeMDG = new List<TestMGD>
            {
                new TestMGD{ ID = 1, Nom = "Item 1", CheminImage = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "Wismerhill.jpg")},
                new TestMGD{ ID = 2, Nom = "Item 2", CheminImage = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "Wismerhill.jpg")},
                new TestMGD{ ID = 3, Nom = "Item 3", CheminImage = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "Wismerhill.jpg")},
                new TestMGD{ ID = 4, Nom = "Item 4", CheminImage = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "Wismerhill.jpg")},
            };

            testMGDBindingSource.DataSource = lesItemsDeMDG;//binding

            bgw = new BackgroundWorker();

            bgw.DoWork += Bgw_DoWork;
            bgw.RunWorkerAsync();
        }

        /// <summary>
        /// Chargement des images en arrière plan
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Bgw_DoWork(object sender, DoWorkEventArgs e)
        {
            for (int i = 0; i < lesItemsDeMDG.Count; i++)
            {
                TestMGD item = lesItemsDeMDG[i];
                Thread.Sleep(2000);//pour simuler un temps long
                Bitmap bmp = new Bitmap(item.CheminImage);
                item.Image = new Bitmap(bmp, new Size(bmp.Width / 10, bmp.Height / 10));//mon image est beaucoup trop grande

                //seul le thread principal peut agrandir la ligne pour s'adapter à l'image, alors on invoke
                dataGridView1.Invoke
                (
                    new Action(() =>
                      {
                          dataGridView1.Rows[i].Height = item.Image.Height;
                      }
                    )
                );
            }
        }
    }

}


Et voilà le résulat


Beau gosse, non?
0
Rejoignez-nous