Invalid cross-thread access issue
Asked Answered
A

3

26

I have two ViewModel classes : PersonViewModel and PersonSearchListViewModel. One of the fields PersonViewModel implements is a profile image that is downloaded via WCF(cached locally in isolated storage). The PersonSearchListViewModel is a container class that holds a list of Persons. Since loading images is relatively heavy, PersonSearchListViewModel loads only images for the current, next and previous page (results are paged on UI)...to further improve the load of images I put the load of images on another thread. However multi-threading approach causes cross-thread access issues.

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();
        }
    }
}

How do you process data in ViewModel class in multi-threaded manner ? I know you have to use dispatcher on UI to update. How do you update ViewModel that is bound to UI ?

** EDIT **

There is also one more weird error happening. In the code below :

        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);
            }
        }

I get a cross-thread error when trying to create a new BitmapImage object in background thread. Why can't I create a new BitmapImage object on a background thread ?

Awash answered 17/12, 2009 at 20:36 Comment(0)
C
62

In order to update a DependencyProperty in a ViewModel, use the same dispatcher you would use to access any other UIElement:

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

Also, BitmapImages have to be instantiated on the UI thread. This is because it uses DependencyProperties, which can only be used on the UI thread. I have tried instantiating BitmapImages on separate threads and it just doesn't work. You could try to use some other means to store images in memory. For example, when you download the image, store it in a MemoryStream. Then a BitmapImage on the UI thread can set its source to the MemoryStream.

You may try instantiating the BitmapImages on the UI thread and then do everything else with the BitmapImage on another thread... but that would get hairy and I'm not even sure it would work. The following would be an example:

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);
Carefree answered 18/12, 2009 at 1:57 Comment(3)
Great, that's exactly what I was looking for. I went with byte[] as a mean of storage.Awash
this also helped me when I had to raise a PropertyChanged event for multiple properties after one was set.Ladida
I did not realize a BitmapImage needed to be made on the UI thread. I really should have thought of that since a DependencyProperty would be an issue otherwise. ::facepalm::Tobi
B
2

I believe you are having a cross threading issue with the UI thread.

Editing the bound object may force an update of the UI on the worker thread, which cannot succeed. You will likely need to do the InvokeRequired/Invoke hokey-pokey whenever you update the bound class.

You said you knew this already, but for reference:

MSDN on thread-safe calls to UI

Birdlime answered 17/12, 2009 at 21:4 Comment(0)
W
0

It can be achieved with 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);  
                });  
            }  
        });  
    }

But You have to construct WriteableBitmap in UI Thread, then loading can be performed in other thread.

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

See more explanaition on this blog post

Wallah answered 12/9, 2011 at 9:22 Comment(2)
I found no explanation in the linked "blog post". Could be that the original address has already been reused to something else - or the entry is well hiddenAgamic
juhariis, i've update blog link, sorry for inconvenienceWallah

© 2022 - 2024 — McMap. All rights reserved.