MarcMan
MarcMan

Reputation: 5

WPF Image display not updated after source is changed

In my MainView there is a ContentControl which is bound to a CurrentView object. The CurrentView is changed via buttons on the MainView bound to commands.

MainView

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

MainVM

(The ObservableObject class implements INotifyPropertyChanged and the RelayCommand class 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;
            });
    }
 }

Those (sub) VM are bound to UserControls which contain image controls and a button to load the image sources from files.

View1

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

View

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

Very neat.

Upvotes: 0

Views: 1422

Answers (3)

lidqy
lidqy

Reputation: 2453

If you want to display all the images of the _images collection of the current view model (which is the "current view") I would display them in a ListBox and put the image tag and the binding into the ListBox's ItemTemplate.
Also as previously mentioned by others, using an ObservableCollecztion<ImageSource> is strongly recommended since your collection data changes and you want your UI to notice it.

If you don't use an ObservableCollection, the view and images get only updated if the view model as a whole changes.

Upvotes: 0

EldHasp
EldHasp

Reputation: 7908

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;
        [...]
    }
}

This implementation has a drawback - it creates a new collection of images each time.
From memory, it doesn't matter (compared to other WPF costs). But replacing a collection results in a re-creation of the UI elements that represent it.
And this is already significantly longer delays.
For your task, this is also not important, since this happens very rarely.

But for more loaded scenarios, it is better to use an observable collection (INotifyCollectionChanged) or a bindable list (IBindingList).
It is typical for WPF to use ObservableCollection<T>.

But in the case of asynchronous work with them, you need to take measures to work with it was thread-safe.
For the implementation I have shown, thread safety is not needed.
This is already implemented in the Binding mechanism itself to work with the INotifyPropertyChanged interface.

Upvotes: 0

Clemens
Clemens

Reputation: 128013

The ContentChanged event is useless.

Declare the Images property like this:

private ImageSource[] images;

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

In LoadImages(), just assign a new array:

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

Upvotes: 1

Related Questions