【问题标题】:WPF Image display not updated after source is changed更改源后 WPF 图像显示未更新
【发布时间】:2021-07-01 13:41:00
【问题描述】:

在我的 MainView 中有一个 ContentControl 绑定到 CurrentView 对象。 CurrentView 通过绑定到命令的 MainView 上的按钮进行更改。

主视图

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

主虚拟机

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

那些(子)VM 绑定到 UserControls,其中包含图像控件和一个用于从文件加载图像源的按钮。

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

虚拟机1

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

现在的问题是图像在加载后没有立即显示。只有在我将 ContentControl 的内容更改为另一个视图然后返回 View1 后,才会显示图像。

有没有办法在加载完成后立即触发显示而不更改 ContentControl 的内容?

编辑:每次用户想要通过按钮加载新图像时都应该这样做,而不仅仅是在初始化期间。

编辑: 使用 lidqy 和 EldHasp 的 cmets,我能够使用 ObservableCollectionItemsControl 清理 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>

非常整洁。

【问题讨论】:

  • 尝试在 ViewModel1 构造函数中调用LoadImages()
  • 请注意,您不应该在视图模型中使用 BitmapImage 类型。相反,请使用基类 ImageSource,或者如果您确实需要访问特定于位图的属性,请使用 BitmapSource。
  • 除此之外,ContentChanged 事件背后的魔法是什么?它曾经被任何组件订阅过吗?不应该有类似OnPropertyChanged(nameof(Images))的东西吗?
  • @LeiYang 是的,这有效,但在初始化期间只有一次。我澄清了我的问题。
  • 另请注意,UpdateSourceTrigger=PropertyChanged 和 Mode=TwoWay 对 Image 元素的 Source Binding 毫无意义。

标签: c# wpf xaml


【解决方案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 成本相比)。 但是替换集合会导致重新创建代表它的 UI 元素。
    这已经是明显更长的延迟了。
    对于您的任务,这也不重要,因为这种情况很少发生。

    但对于负载较多的场景,最好使用可观察集合(INotifyCollectionChanged)或可绑定列表(IBindingList)。
    WPF 通常使用ObservableCollection&lt;T&gt;

    但是在与它们进行异步工作的情况下,您需要采取措施使其工作是线程安全的。
    对于我展示的实现,不需要线程安全。
    这已经在 Binding 机制本身中实现,以便与 INotifyPropertyChanged 接口一起使用。

    【讨论】:

    • 替换集合会导致重新创建代表它的 UI 元素”对于 ItemsControl 来说是正确的,但在这里不是。
    • 感谢您的解释,连同 Clemens 的回答,它可以在没有 ContentChanged 事件的情况下工作。
    • 另一个实现 UserControl 的建议。您在 XAML 中显式指定几个单独的 Image,并通过元素的索引将它们的源绑定到集合。在 WPF 中,最好在这种情况下使用 ItemsControl。除了简化 XAML 代码之外,您还可以自动更改显示项目的数量。
    【解决方案3】:

    如果您想显示当前视图模型(即“当前视图”)的_images 集合的所有图像,我会将它们显示在一个ListBox 并将图像标签和绑定放入ListBox 的ItemTemplate
    正如其他人之前提到的,强烈建议使用ObservableCollecztion&lt;ImageSource&gt;,因为您的集合数据会发生变化,并且您希望您的 UI 能够注意到它。

    如果您不使用 ObservableCollection,则仅当整个视图模型发生更改时,视图和图像才会更新。

    【讨论】:

    • 最好使用 ItemsControl,除非您还希望能够选择图像。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-11-28
    • 2015-05-30
    • 1970-01-01
    • 2014-07-13
    • 1970-01-01
    • 2011-06-06
    相关资源
    最近更新 更多