非法的跨线程访问问题

26
我有两个ViewModel类:PersonViewModel和PersonSearchListViewModel。其中一个字段PersonViewModel实现了通过WCF下载的个人资料图像(在隔离存储中缓存)。PersonSearchListViewModel是一个包含Person列表的容器类。由于加载图像相对较重,PersonSearchListViewModel仅加载当前、下一页和上一页的图像(结果在UI上进行分页)...为了进一步改善图像的加载,我将图像的加载放在了另一个线程上。然而,多线程方法会导致跨线程访问问题。
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();
        }
    }
}
如何在多线程环境中以ViewModel类的方式处理数据?我知道你必须使用UI调度程序来更新。如何更新绑定到UI的ViewModel?
** 编辑 **
还有一个更奇怪的错误发生。 在下面的代码中:
        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);
            }
        }

我在后台线程中尝试创建新的BitmapImage对象时出现了跨线程错误。为什么不能在后台线程中创建新的BitmapImage对象?

3个回答

62

为了在ViewModel中更新DependencyProperty,请使用与访问任何其他UI元素相同的调度程序:

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

此外,BitmapImages 必须在 UI 线程上实例化。这是因为它使用了 DependencyProperties,而 DependencyProperties 只能在 UI 线程上使用。我试过在其他线程上实例化 BitmapImages,但并没有成功。你可以尝试使用其他方式来存储图像。例如,在下载图像时将其存储在 MemoryStream 中。然后在 UI 线程上,可以使用 BitmapImage 将其源设置为 MemoryStream。

你可以尝试在 UI 线程上实例化 BitmapImages,然后将其他操作放到另一个线程中进行... 但这会变得复杂,我甚至不确定它是否有效。以下是一个示例:

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

太好了,这正是我在寻找的。我选择使用byte[]作为存储手段。 - Ender
当我需要在设置一个属性后为多个属性引发PropertyChanged事件时,这也帮助了我。 - Scott M.
我没有意识到BitmapImage需要在UI线程上创建。否则,依赖属性将会是一个问题,我真的应该想到这一点。 ::facepalm:: - Paul

2
我相信你正在遇到UI线程的交叉线程问题。
在工作线程上编辑绑定对象可能会强制更新UI,但这是不可能成功的。每当你更新绑定类时,你很可能需要使用InvokeRequired/Invoke技巧。
你说你已经知道了这一点,但供参考: MSDN关于对UI进行线程安全调用

0
可以使用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);  
                });  
            }  
        });  
    }

但是您必须在 UI 线程中构造 WriteableBitmap,然后可以在其他线程中执行加载操作。

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

在这个博客文章中了解更多相关解释。


在链接的“博客文章”中我没有找到任何解释。可能原始地址已经被重用为其他内容,或者该条目被隐藏得很好。 - juhariis
juhariis,我已经更新了博客链接,对不便表示抱歉。 - Ernest Poletaev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接