Недопустимая проблема с доступом к нескольким потокам
У меня есть два класса ViewModel: PersonViewModel и PersonSearchListViewModel. Одним из полей PersonViewModel реализуется образ профиля, который загружается через WCF (кэшируется локально в изолированном хранилище). PersonSearchListViewModel - это контейнерный класс, в котором содержится список лиц. Поскольку загрузка изображений относительно тяжелая, PersonSearchListViewModel загружает только изображения для текущей, следующей и предыдущей страницы (результаты отображаются в пользовательском интерфейсе)... для дальнейшего улучшения загрузки изображений я помещаю загрузку изображений в другой поток. Однако многопоточный подход вызывает проблемы с доступом к нескольким потокам.
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 многопоточным способом? Я знаю, что вам нужно использовать диспетчер в пользовательском интерфейсе для обновления. Как вы обновляете 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 в фоновом потоке?
Ответы
Ответ 1
Чтобы обновить DependencyProperty в ViewModel, используйте тот же диспетчер, который вы использовали бы для доступа к любому другому UIElement:
System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() => {...});
Кроме того, BitmapImages необходимо создать в потоке пользовательского интерфейса. Это связано с тем, что он использует DependencyProperties, который может использоваться только в потоке пользовательского интерфейса. Я попытался создать экземпляр BitmapImages для отдельных потоков, и он просто не работает. Вы можете попытаться использовать некоторые другие средства для хранения изображений в памяти. Например, при загрузке изображения сохраните его в MemoryStream. Затем BitmapImage в потоке пользовательского интерфейса может установить его источник в MemoryStream.
Вы можете попробовать создать экземпляр BitmapImages в потоке пользовательского интерфейса, а затем сделать все остальное с помощью BitmapImage в другом потоке... но это будет волосатое, и я даже не уверен, что это сработает. Ниже приведен пример:
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);
Ответ 2
Я считаю, что у вас проблема с перекрестными потоками с потоком пользовательского интерфейса.
Редактирование связанного объекта может привести к обновлению пользовательского интерфейса рабочего потока, что не может быть выполнено. Вероятно, вам придется выполнять InvokeRequired/Invoke hokey-pokey всякий раз, когда вы обновляете связанный класс.
Вы сказали, что знаете это уже, но для справки:
MSDN для потокобезопасных вызовов в пользовательский интерфейс
Ответ 3
Это может быть достигнуто с помощью 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);
});
}
});
}
Но вам нужно построить WriteableBitmap в потоке пользовательского интерфейса, тогда загрузка может быть выполнена в другом потоке.
void DeferImageLoading( Stream imgStream )
{
// we have to give size
var bmp = new WriteableBitmap(80, 80);
imageThread.LoadThumbAsync(imgStream, bmp, this);
}
Подробнее об этом говорится в блоге