Отображение изображения WPF не обновляется после изменения источника

#c# #wpf #xaml

Вопрос:

В моем главном представлении есть ContentControl, который привязан к объекту currentView. Текущее представление изменяется с помощью кнопок на главном представлении, привязанных к командам.

Основной вид

 <Window>(...)
       <RadioButton Content="View1"
                    Command="{Binding View1Command}"/>
       <RadioButton Content="View2" 
                    Command="{Binding View2Command}"/>
   <ContentControl Content="{Binding CurrentView}"/>
</Window>
 

MainVM

(Класс ObservableObject реализует INotifyPropertyChanged, а класс RelayCommand-ICommand.)

 class MainViewModel : ObservableObject
{
        public RelayCommand ViewCommand1 { get; set; }
        public RelayCommand ViewCommand2 { get; set; }

        public ViewModel2 VM1 { get; set; }
        public ViewModel2 VM2 { get; set; }
        
        object _currentView;
        
        public object CurrentView
        {
            get { return _currentView; }
            set 
            { 
                _currentView = value;
                OnPropertyChanged();
            }
        }
    public MainViewModel()
    {
      VM1 = new ViewModel1();
      VM1.ContentChanged  = (s, e) => OnPropertyChanged();
      ViewCommand1 = new RelayCommand(o =>
            {
                CurrentView = VM1;
            });

      VM2 = new ViewModel2();
      ViewCommand2 = new RelayCommand(o =>
            {
                CurrentView = VM2;
            });
    }
 }
 

Эти (вспомогательные) виртуальные машины привязаны к элементам управления пользователями, которые содержат элементы управления изображениями и кнопку для загрузки источников изображений из файлов.

Просмотр1

 <UserControl x:Class="Project.Views.View1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:viewModels="clr-namespace:Project.ViewModels" 
             d:DataContext="{d:DesignInstance Type=viewModels:ViewModel1}"
             mc:Ignorable="d" >
[...]
  <Button Command="{Binding LoadImagesCommand}"/>
[...]
  <Image Source="{Binding Images[0]}" "/>
  <Image Source="{Binding Images[1]}" "/>
[...]
</UserControl>
 

VM1

 class RiJustageViewModel: ObservableObject
{
    public event EventHandler ContentChanged;
    void OnContentChanged()
    {
        ContentChanged?.Invoke(this, new EventArgs());
    }

    public RelayCommand LoadImagesCommand { get; set; }
    
    public ViewModel1()
    {
        Images = new BitmapImage[9];
        LoadImagesCommand = new RelayCommand(o => LoadImages());
    }

    BitmapImage[] _images;

    public BitmapImage[] Images
    {
        get { return _images; }
        set
        {
            _images = value;
            OnContentChanged();
        }
    }

    public void LoadImages()
    {  
        [...]
        for (int i = 0; i < files.Length; i  )
        {
           Images[i] = Utility.BmImageFromFile(files[i]);
        }
        [...]
    }
}
 

The issue now is that the images are not shown right away after they are loaded. Only after I change the content of the ContentControl to another view and then back to View1 the images are shown.

Is there a way to trigger that display right after the loading is complete without changing the content of the ContentControl?

EDIT:This should be done everytime the user wants to load new images via the button, not only during initialization.

EDIT:
With lidqy’s and EldHasp’s comments I was able to clean up the VM and the View using ObservableCollection and ItemsControl .

VM

   public class ImageItem
    {
        public string FileName{ get; set; }
        public ImageSource Image { get; set; }
        public ImageItem(string f, ImageSource im)
        {
            FileName = f;
            Image = im;
        }
    }

    public ObservableCollection<ImageItem> ImageItems { get; set; }

   [...]
   public void LoadImages()
   {
     [...]
     ImageItems.Clear();
     foreach (var file in files)
     {
        var im = Utility.BmImageFromFile(file);
        var f = Path.GetFileName(file);
        ImageItems.Add(new ImageItem(f, im));
     }
}
 

Вид

 <ItemsControl ItemsSource="{Binding ImageItems}">
    <ItemsControl.ItemsPanel>
      <ItemsPanelTemplate>
        <UniformGrid Columns="3" Rows="3"/>
      </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
            
    <ItemsControl.ItemTemplate>
      <DataTemplate>
        <Grid Margin="5">
          <Grid.ColumnDefinitions>
            <ColumnDefinition Width="400"/>
          </Grid.ColumnDefinitions>
          <Grid.RowDefinitions>
            <RowDefinition Height="18" />
            <RowDefinition Height="200" />
          </Grid.RowDefinitions>
          <TextBlock Text="{Binding FileName}" Style="{StaticResource ImageDescr}" />
          <Image Grid.Row="1" Source="{Binding Image}" Style="{StaticResource ImageTheme}" />
         </Grid>
       </DataTemplate>
     </ItemsControl.ItemTemplate>
  </ItemsControl>
 

Очень аккуратно.

Комментарии:

1. попробуйте вызвать LoadImages() конструктор ViewModel1.

2. Обратите внимание, что вы не должны использовать растровое изображение типа в своей модели представления. Вместо этого используйте базовый класс ImageSource, или, если вам действительно нужен доступ к свойствам растрового изображения, используйте BitmapSource.

3. Кроме того, какова предполагаемая магия, стоящая за событием ContentChanged? На него когда-либо подписывался какой-либо компонент? Разве вместо этого не должно быть чего-то подобного OnPropertyChanged(nameof(Images)) .

4. @LeiYang Да, это работает, но только один раз во время инициализации. Я уточнил свой вопрос.

5. Также имейте в виду, что как UpdateSourceTrigger=PropertyChanged, так и Mode=TwoWay бессмысленны для привязки источника элемента изображения.

Ответ №1:

ContentChanged Мероприятие бесполезно.

Объявите Images свойство следующим образом:

 private ImageSource[] images;

public ImageSource[] Images
{
    get { return images; }
    set
    {
        images = value;
        OnPropertyChanged();
    }
}
 

В LoadImages() , просто назначьте новый массив:

 public void LoadImages()
{
    ...  
    Images = files
        .Select(f => Utility.BmImageFromFile(f))
        .ToArray();
}
 

Ответ №2:

 class RiJustageViewModel: ObservableObject
{
    public event EventHandler ContentChanged;
    void OnContentChanged()
    {
        ContentChanged?.Invoke(this, new EventArgs());
    }

    public RelayCommand LoadImagesCommand { get; set; }
    
    public ViewModel1()
    {
        // If this is not an observable collection,
        // then it makes no sense to create it in advance.
        // Images = new BitmapImage[9];

        LoadImagesCommand = new RelayCommand(o => LoadImages());
    }

    // Since it is not an observable collection,
    // the more general interface can be used: IEnumerable or IEnumerable <T>.
    IEnumerable<BitmapImage> _images;

    public IEnumerable<BitmapImage> Images
    {
        get { return _images; }
        set
        {
            _images = value;
            OnContentChanged();
        }
    }

    public void LoadImages()
    {  
        [...]
        // For an unobservable collection,
        // changing elements does not automatically change their presentation.
        // We need to create a NEW collection with
        // new elements and assign it to the property.
        BitmapImage[] localImages = new BitmapImage[files.Length];
        for (int i = 0; i < files.Length; i  )
        {
           localImages[i] = Utility.BmImageFromFile(files[i]);
        }
        Images = localImages;
        [...]
    }
}
 

У этой реализации есть недостаток — она каждый раз создает новую коллекцию изображений.
Из памяти это не имеет значения (по сравнению с другими затратами WPF).
Но замена коллекции приводит к воссозданию элементов пользовательского интерфейса, которые ее представляют.
И это уже значительно более длительные задержки.
Для вашей задачи это тоже не важно, так как такое случается очень редко.

Но для более загруженных сценариев лучше использовать наблюдаемую коллекцию (INotifyCollectionChanged) или привязываемый список (IBindingList).
Это типично для использования WPF ObservableCollection<T> .

Но в случае асинхронной работы с ними нужно принять меры, чтобы работа с ними была потокобезопасной.
Для реализации, которую я показал, безопасность потоков не требуется.
Это уже реализовано в самом механизме привязки для работы с интерфейсом INotifyPropertyChanged.

Комментарии:

1. » замена коллекции приводит к воссозданию элементов пользовательского интерфейса, которые ее представляют «, было бы верно для элемента управления элементами, но не здесь.

2. Спасибо за объяснение, вместе с ответом Клеменса он работает без события ContentChanged.

3. Еще одна рекомендация по внедрению UserControl. Вы явно указываете несколько отдельных изображений в XAML с привязкой их источника к коллекции по индексу элементов. В WPF в таких случаях лучше использовать ItemsControl. В дополнение к упрощению кода XAML, вы также дополнительно получаете автоматическое изменение количества отображаемых элементов.

Ответ №3:

Если вы хотите отобразить все изображения _images коллекции текущей модели представления (которая является «текущим представлением»), Я бы отобразил их в a ListBox и поместил тег изображения и привязку в список ItemTemplate .
Также, как ранее упоминалось другими, ObservableCollecztion<ImageSource> настоятельно рекомендуется использовать an, поскольку данные вашей коллекции меняются, и вы хотите, чтобы ваш пользовательский интерфейс это заметил.

Если вы не используете коллекцию ObservableCollection, представление и изображения обновляются только в том случае, если изменяется модель представления в целом.

Комментарии:

1. Лучше используйте элемент управления, если вы также не хотите иметь возможность выбирать изображения.