WPF невозможно обновить элемент окна

181
24 октября 2018, 22:00

Использовал слайдер на DispatcherTimer внутри кода окна. Но после переноса в отдельный Singleton класс потоков управляющих кэшированием картинок и смены слайда выскакивает ошибка.

Помогите побороть эту ошибку возникающую при передачи изображения в форму. Не понимаю, что делаю не так. Вроде оформил доступ к окну через делегат. И вроде подобные структуры работают нормально с текстовыми полями формы, но с изображением нет.

Код MainWindow.xaml.cs:

public partial class MainWindow : Window
{
    private delegate void slideCacheUpdateDelegate(int slide, BitmapImage image);
    public MainWindow()
    {
        SlideCache.getInstance.init();
        InitializeComponent();
        // Initial slide cache updater delegate.
        SlideCache.OnRefresh += (s, i) =>
        {
            if (Dispatcher.CheckAccess())
                changeToNextSlide(s, i);
            else
                Dispatcher.BeginInvoke(new slideCacheUpdateDelegate(changeToNextSlide), new object[] { s, i });
        };
        SlideCache.getInstance.startSlideShow();
    }
    private void changeToNextSlide(int slide, BitmapImage image)
    {
        switch (slide)
        {
            case 1:
                this.imageSlide2.Source = image;
                DoubleAnimation animationSlide1 = new DoubleAnimation();
                animationSlide1.From = 0;
                animationSlide1.To = 1;
                animationSlide1.Duration = TimeSpan.FromMilliseconds(500);
                this.imageSlide2.BeginAnimation(Canvas.OpacityProperty, animationSlide1);
                break;
            case 2:
                this.imageSlide1.Source = image;
                DoubleAnimation animationSlide2 = new DoubleAnimation();
                animationSlide2.From = 1;
                animationSlide2.To = 0;
                animationSlide2.Duration = TimeSpan.FromMilliseconds(500);
                this.imageSlide2.BeginAnimation(Canvas.OpacityProperty, animationSlide2);
                break;
        }
    }
}

Код MainWindow.xaml:

<Window x:Class="SlideShow.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <StackPanel Width="200" Height="100" Background="Black">
            <Image Name="imageSlide1" Stretch="Fill"/>
            <Image Name="imageSlide2" Stretch="Fill"/>
        </StackPanel>
    </Grid>
</Window>

Код SlideCache.cs:

public class SlideCache
{
    public delegate void Slider(int slide, BitmapImage holder);
    public static event Slider OnRefresh;
    private static volatile SlideCache INSTANCE;
    private static object syncRoot = new Object();
    private Thread _threadCache;
    private Thread _threadSlide;
    private Dictionary<int, BitmapHolder> _images;
    private int _currentSlide;
    private Boolean _isFirstImage;
    private SlideCache()
    {
        // Do nothing.
    }
    public void init()
    {
        initDictionary();
        _currentSlide = 0;
        _isFirstImage = true;
        _threadCache = new Thread(slideCache);
        _threadCache.IsBackground = true;
        _threadCache.SetApartmentState(ApartmentState.MTA);
        _threadCache.Start();
    }
    private void slideShow()
    {
        while (true)
        {
            ++_currentSlide;
            if (_images[_currentSlide] != null)
            {
                slideUpdate(_isFirstImage ? 1 : 2, _images[_currentSlide].getImage());
                _isFirstImage = !_isFirstImage;
            }
            Thread.Sleep(3000);
        }
    }
    private void slideCache()
    {
        while (true)
        {
            DateTime timestamp = DateTime.Now;
            for (int i = 0; i < 10; i++ )
            {
                int slideId = i + 1;
                BitmapHolder holder = _images[slideId];
                if (holder == null || holder.getTimestamp() < timestamp)
                {
                    BitmapImage image = new BitmapImage();
                    image.BeginInit();
                    image.UriSource = new Uri("http://example.com/slide_" + slideId);
                    image.EndInit();
                    if (holder != null && holder.getImage() != null && image.PixelWidth == 1)
                        continue;
                    _images[slideId] = new BitmapHolder(timestamp.AddSeconds(new Random().Next(180, 600)), image);
                }
            }
            Thread.Sleep(1000);
        }
    }
    private void initDictionary()
    {
        _images = new Dictionary<int, BitmapHolder>(10);
        for (int i = 0; i < 10; i++)
            _images.Add((i + 1), null);
    }
    private void slideUpdate(int slide, BitmapImage image)
    {
        OnRefresh.Invoke(slide, image);
    }
    public void startSlideShow()
    {
        _threadSlide = new Thread(slideShow);
        _threadSlide.IsBackground = true;
        _threadSlide.SetApartmentState(ApartmentState.MTA);
        _threadSlide.Start();
    }
    public void stopSlideShow()
    {
        _threadSlide.Abort();
    }
    public void changeToNextSlide(int slide)
    {
        _currentSlide = slide;
        slideUpdate(_isFirstImage ? 1 : 2, _images[_currentSlide].getImage());
        _isFirstImage = !_isFirstImage;
    }
    public Boolean slideShowEnabled()
    {
        return _threadSlide.ThreadState == ThreadState.Running;
    }
    public static SlideCache getInstance
    {
        get
        {
            if (INSTANCE == null)
            {
                lock (syncRoot)
                {
                    if (INSTANCE == null)
                        INSTANCE = new SlideCache();
                }
            }
            return INSTANCE;
        }
    }
}

Код BitmapHolder.cs:

public class BitmapHolder
{
    private DateTime _timestamp;
    private BitmapImage _image;
    public BitmapHolder(DateTime timestamp, BitmapImage image)
    {
        _timestamp = timestamp;
        _image = image;
    }
    public DateTime getTimestamp()
    {
        return _timestamp;
    }
    public BitmapImage getImage()
    {
        return _image;
    }
}

При запуске приложение зависает и выскакивает следущая ошибка в VS:

System.InvalidOperationException не обработано пользовательским кодом Вызывающий поток не может получить доступ к данному объекту, так как владельцем этого объекта является другой поток.

Решение проблемы:

BitmapImage image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = new MemoryStream(new WebClient().DownloadData("http://example.com/Slide_" + slideId + ".png"));
image.EndInit();
image.Freeze();

Спасибо MSDN.WhiteKnight за помощь с проверкой локальных ресурсов. Ваш ответ был ближе всего к решению проблемы.

P.S. Сама проблема была не с доступом к элементу UI из другого потока, а с блокированным ресурсом который поток из Singleton класса пытался передать в UI. Т.е. нужно было взять картинку как стрим поток и на основании его создать ресурс. А далее как в MSDN описании Freeze() для совместного использования. Надеюсь кому-то пригодится, т.к. проблема не единичная, по крайней мере, на EN SO.

Answer 1

Объект ImageSource обычно может использоваться только из потока, в котором он был создан. Необходимо вызвать image.Freeze() после завершения загрузки изображения (т.е., вызова image.EndInit()), если вы хотите использовать его в других потоках (после этого он станет неизменяемым). См. Freezable Objects Overview

Также, если ImageSource указывает на HTTP URL, его загрузка осуществляется асинхронно. Поэтому перед вызовом Freeze нужно дождаться его загрузки, при этом выполняя обработку событий WPF в потоке. Для этого понадобится вспомогательный метод:

using System.Windows.Threading;
public static void DoWpfEvents()
{
      DispatcherFrame frame = new DispatcherFrame();
      Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
           new DispatcherOperationCallback((f) =>
           {
               ((DispatcherFrame)f).Continue = false; return null;
           }), frame);
      Dispatcher.PushFrame(frame);
 } 

Ожидание можно выполнить так:

BitmapImage image = new BitmapImage();
image.BeginInit();
image.UriSource = new Uri("http://example.com/image_" + slideId.ToString() + ".png");
image.CacheOption = BitmapCacheOption.OnLoad;
image.EndInit();
while (image.IsDownloading) { DoWpfEvents(); Thread.Sleep(100); }                     
image.Freeze();

Но лучше воспользоваться советом из соседнего ответа и создать изображение в основном потоке (он на самом деле работает, если все сделать правильно):

BitmapImage image=null;
Application.Current.Dispatcher.Invoke(() => {
    image = new BitmapImage();
    image.BeginInit();
    image.UriSource = new Uri("http://example.com/image_" + slideId.ToString() + ".png");        
    image.EndInit();
});
Answer 2

Любое взаимодействие с UI из потока, отличного от потока UI, должно вызываться в UI потоке. Проверяйте весь код, где есть какое-либо взаимодействие с UI контролами, например, присвоение картинки и используйте Dispatcher.

App.Current.Dispatcher.Invoke(() => imageSlide1.Source = CreateImage());
READ ALSO
Binding in triggers

Binding in triggers

Я описую шаблон для кнопки, хочу чтобы цвет кнопки при наведении и в нормальном состоянии задавался пользователемНо при установке тригерров...

179
Пагинация на сайте

Пагинация на сайте

задание сделать библиотеку(реальная с книгами и газетами) с использованием трехслойной архитектурой, где UI - asp net mvcПервым заданием делал...

173
MySQL SELECT JOIN 3 таблицы, LIMIT 1

MySQL SELECT JOIN 3 таблицы, LIMIT 1

Всем добраВозникли трудности, работая с 3-мя таблицами

148
Маска ввода textbox для телефона WPF

Маска ввода textbox для телефона WPF

У меня есть textbox в который из бд выводится номер телефона:

319