web-dev-qa-db-fra.com

Problème d'accès cross-thread non valide

J'ai deux classes ViewModel: PersonViewModel et PersonSearchListViewModel. Un des champs implémentés par PersonViewModel est une image de profil téléchargée via WCF (mise en cache localement dans un stockage isolé). PersonSearchListViewModel est une classe de conteneur qui contient une liste de personnes. Puisque le chargement des images est relativement lourd, PersonSearchListViewModel charge uniquement les images de la page en cours, de la page suivante et de la page précédente (les résultats sont paginés sur l'interface utilisateur) ... pour améliorer encore la charge des images, j'ai placé la charge des images sur un autre thread. Cependant, l'approche multi-threading pose des problèmes d'accès multi-thread. 

PersonViewModel:

public void RetrieveProfileImage()
{
    Image profileImage = MemorialDataModel.GetImagePerPerson(Person);
    if (profileImage != null)
    {
        MemorialDataModel.ImageManager imgManager = new MemorialDataModel.ImageManager();
        imgManager.GetBitmap(profileImage, LoadProfileBitmap);
    }
}

private void LoadProfileBitmap(BitmapImage bi)
{
    ProfileImage = bi;
    // update 
    IsProfileImageLoaded = true;
}

private BitmapImage profileImage;
public BitmapImage ProfileImage
{
    get
    {
        return profileImage;
    }
    set
    {
        profileImage = value;
        RaisePropertyChanged(new System.ComponentModel.PropertyChangedEventArgs("ProfileImage"));
    }
}

PersonSearchListViewModel:

private void LoadImages()
{
    // load new images 
    Thread loadImagesThread = new Thread(new ThreadStart(LoadImagesProcess));
    loadImagesThread.Start();

    //LoadImagesProcess(); If executed on the same thread everything works fine 
}

private void LoadImagesProcess()
{
    int skipRecords = (PageIndex * PageSize);
    int returnRecords;

    if (skipRecords != 0)
    {
        returnRecords = 3 * PageSize; // page before, cur page and next page 
    }
    else
    {
        returnRecords = 2 * PageSize;   // cur page and next page 
    }

    var persons = this.persons.Skip(skipRecords).Take(returnRecords);

    // load images 
    foreach (PersonViewModel pvm in persons)
    {
        if (!pvm.IsProfileImageLoaded)
        {
            pvm.RetrieveProfileImage();
        }
    }
}

Comment traitez-vous les données de la classe ViewModel de manière multithread? Je sais que vous devez utiliser le répartiteur sur l'interface utilisateur pour mettre à jour. Comment mettre à jour ViewModel lié à l'interface utilisateur? 

** MODIFIER **

Il se produit également une autre erreur étrange. Dans le code ci-dessous: 

        public void GetBitmap(int imageID, Action<BitmapImage> callback)
        {
            // Get from server 
            bitmapCallback = callback;

            memorialFileServiceClient.GetImageCompleted += new EventHandler<GetImageCompletedEventArgs>(OnGetBitmapHandler);
            memorialFileServiceClient.GetImageAsync(imageID);
        }

        public void OnGetBitmapHandler(object sender, GetImageCompletedEventArgs imageArgs)
        {
            if (!imageArgs.Cancelled)
            {
                // I get cross-thread error right here 
                System.Windows.Media.Imaging.BitmapImage bi = new BitmapImage();
                ConvertToBitmapFromBuffer(bi, imageArgs.Result.Image);

                // call call back
                bitmapCallback.Invoke(bi);
            }
        }

Je reçois une erreur transversale lorsque j'essaie de créer un nouvel objet BitmapImage dans le fil d'arrière-plan. Pourquoi ne puis-je pas créer un nouvel objet BitmapImage sur un thread d'arrière-plan? 

26
Ender

Pour mettre à jour un DependencyProperty dans un ViewModel, utilisez le même répartiteur que celui que vous utiliseriez pour accéder à tout autre UIElement:

System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() => {...});

De plus, les BitmapImages doivent être instanciées sur le thread d'interface utilisateur. En effet, il utilise DependencyProperties, qui ne peut être utilisé que sur le thread d'interface utilisateur. J'ai essayé d'instancier BitmapImages sur des threads distincts et cela ne fonctionne tout simplement pas. Vous pouvez essayer d’utiliser un autre moyen pour stocker des images en mémoire. Par exemple, lorsque vous téléchargez l'image, stockez-la dans un MemoryStream. Ensuite, une BitmapImage sur le thread d'interface utilisateur peut définir sa source sur MemoryStream.

Vous pouvez essayer d'instancier les BitmapImages sur le thread d'interface utilisateur, puis tout faire avec BitmapImage sur un autre thread ... mais cela deviendrait poilu et je ne suis même pas sûr que cela fonctionnerait. Ce qui suit serait un exemple:

System.Windows.Media.Imaging.BitmapImage bi = null;
using(AutoResetEvent are = new AutoResetEvent(false))
{
    System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() =>
    {
        bi = new BitmapImage();
        are.Set();
    });
    are.WaitOne();
}

ConvertToBitmapFromBuffer(bi, imageArgs.Result.Image);
bitmapCallback.Invoke(bi);
62
Phil

Je crois que vous rencontrez un problème de threading avec le fil de l'interface utilisateur.

La modification de l'objet lié peut imposer une mise à jour de l'interface utilisateur sur le thread de travail, ce qui ne peut aboutir. Vous devrez probablement exécuter InvokeRequired/Invoke hokey-pokey chaque fois que vous mettez à jour la classe liée.

Vous avez dit que vous le saviez déjà, mais pour référence:

MSDN sur les appels thread-safe vers l'interface utilisateur

2
Scott P

Cela peut être réalisé avec WriteableBitmap.

    public void LoadThumbAsync(Stream src, 
                    WriteableBitmap bmp, object argument)  
    {  
        ThreadPool.QueueUserWorkItem(callback =>  
        {  
            bmp.LoadJpeg(src);  
            src.Dispose();  
            if (ImageLoaded != null)  
            {  
                Deployment.Current.Dispatcher.BeginInvoke(() =>  
                {  
                    ImageLoaded(bmp, argument);  
                });  
            }  
        });  
    }

Mais vous devez construire WriteableBitmap dans le thread d'interface utilisateur, puis le chargement peut être effectué dans un autre thread.

    void DeferImageLoading( Stream imgStream )  
    {  
        // we have to give size  
        var bmp = new WriteableBitmap(80, 80);  
        imageThread.LoadThumbAsync(imgStream, bmp, this);  
    }  

Voir plus d'explications à ce sujet blog post

0
Ernest