将 WrapPanel 用作 ItemsPanel 的 ItemsControl - 结合“静态”子项和 ItemsSource

Posted

技术标签:

【中文标题】将 WrapPanel 用作 ItemsPanel 的 ItemsControl - 结合“静态”子项和 ItemsSource【英文标题】:ItemsControl with WrapPanel as ItemsPanel - combine a "static" child and ItemsSource 【发布时间】:2016-01-27 23:04:54 【问题描述】:

使用带有 WrapPanel 设置为 ItemsPanel 的 ItemsControl,我试图实现此图像中所示的内容:

XAML 看起来像这样(经过修改以使其更简单):

<ItemsControl ItemsSource="Binding Animals">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel IsItemsHost="True" Orientation="Horizontal" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Border Margin="5">
                <Image Source="Binding ImageUrl" />
            </Border>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

底层 ViewModel 如下所示:

public class Zoo

    public ObservableCollection<Animal> Animals  get; set;  = new ObservableCollection<Animal>(); 

    public ICommand AddAnimal() => new DelegateCommand(() => Animals.Add(new Animal()));


public class Animal

    public string ImageUrl  get; set; 

ItemsControl 的 DataContext 设置为 Zoo 的一个实例并填充了 4 只动物。

问题: 如何添加一个看起来像其他元素的“静态”子元素,是 WrapPanel 的子元素,并与其他子元素一起包装?具体来说,我希望最后一个元素是一个添加按钮(上图中显示的绿色加号),它绑定到 Zoo 的 AddAnimal Command 属性。

要求:

add-button-element 必须与 WrapPanel 的其他子元素一起包装。 add-button-element 必须始终是最后一个(或第一个)元素,如果 Zoo 中没有动物,它也应该是可见的。 将虚拟 Animal 添加到 Animals-collection,然后使用样式修改最后一个子项不是一种选择。底层模型在应用程序中的很多地方都用到了,因此在 Animals-collection 中漂浮一个虚拟的 Animal 实在是太骇人听闻了。

我想过:

使用将 Animals 集合复制到新集合中的转换器,将虚拟 Animal 添加到副本中,并将其返回到 ItemsControl 的 ItemsSource 绑定。然后我可以将最后一个元素设置为添加按钮。但是,这不是一个选项,因为某些拖放逻辑需要能够通过 ItemsControl 的 ItemsSource 属性处理原始 ObservableCollection。 子类化 WrapPanel 并将 add-button-element 添加为子元素而不修改 ItemsSource 属性似乎是最好的选择(如果可能的话),但我不知道该怎么做。

其他信息:我使用:WPF、PRISM、C# 6.0、.NET 4.0 Client Profile 和 MVVM 模式。

对这个问题有什么想法吗?

解决方案:

@kyriacos_k 的回答通过两个小修改为我解决了这个问题:

    如果 DataTemplate 是通过 ItemsControl.ItemTemplate 属性设置的,则它不起作用,即添加按钮会拾取 DataTemplate 并显示不正确。我想这是设计使然,正如 MSDN 所述:“ItemsControl 使用 CompositeCollection 中的数据根据​​其 ItemTemplate 生成其内容”,来源:https://msdn.microsoft.com/en-us/library/system.windows.data.compositecollection%28v=vs.110%29.aspx) 我必须使用“绑定代理”才能使 AddAnimal Command 绑定工作。 “直接”绑定语法或相对源都不起作用。我猜这是因为添加按钮不是同一可视树的一部分,并且不知何故它没有从 ItemsControl 中获取 DataContext。

最后这对我有用:

<ItemsControl>
    <ItemsControl.Resources>
        <CollectionViewSource x:Key="AnimalCollection" Source="Binding Animals"/>
        <behaviors:BindingProxy x:Key="Proxy" DataContext="Binding"/>
        <DataTemplate DataType="x:Type local:Animal">
            <Border Margin="5">
                <Image Source="Binding ImageUrl" />
            </Border>
        </DataTemplate>
    </ItemsControl.Resources>
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel IsItemsHost="True" Orientation="Horizontal" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemsSource>
        <CompositeCollection>
            <CollectionContainer Collection="Binding Source=StaticResource AnimalCollection"/>
            <Border Margin="5">
                <Button Command="Binding DataContext.AddAnimal, Source=StaticResource Proxy">
                    <Image Source="SourceToPlusSign"/>
                </Button>
            </Border>
        </CompositeCollection>
    </ItemsControl.ItemsSource>
</ItemsControl>

BindingProxy 的代码在这里(直接从:Binding Visibility for DataGridColumn in WPF 抢夺):

public class BindingProxy : Freezable

    protected override Freezable CreateInstanceCore()
    
        return new BindingProxy();
    

    public object DataContext
    
        get  return GetValue(DataContextProperty); 
        set  SetValue(DataContextProperty, value); 
    

    public static readonly DependencyProperty DataContextProperty =
        DependencyProperty.Register("DataContext", typeof(object),
                                     typeof(BindingProxy));

【问题讨论】:

【参考方案1】:

看看CompositeCollection。它允许您向 ItemsSource 绑定添加其他项目:

<Window.Resources>
   <CollectionViewSource x:Key="AnimalViewSource" Source="Binding Animals"/>
</Window.Resources>

<ItemsControl>
    <ItemsControl.ItemsSource>
        <CompositeCollection>
             <local:Animal ImageUrl="somepath/plussign.png" />
             <CollectionContainer Collection="Binding Source=StaticResource AnimalViewSource"/>
         </CompositeCollection>
    </ItemsControl.ItemsSource>

    ... ItemsPanel, ItemsTemplate, etc. follow here ...
</ItemsControl>

【讨论】:

【参考方案2】:

您可以使用CompositeCollection 以一种非常简洁的方式进行操作

<ItemsControl>
    <ItemsControl.Resources>
        <CollectionViewSource x:Key="AnimalCollection" Source="Binding Animals"/>
    </ItemsControl.Resources>
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel IsItemsHost="True" Orientation="Horizontal" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Border Margin="5">
                <Image Source="Binding ImageUrl" />
            </Border>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
    <ItemsControl.ItemsSource>
        <CompositeCollection>
            <CollectionContainer Collection="Binding Source=StaticResource AnimalCollection"/>
            <Border Margin="5">
                <Button Command="Binding AddAnimal">
                    <Image Source="YourAddButtonSource"/>
                </Button>
            </Border>
        </CompositeCollection>
    </ItemsControl.ItemsSource>
</ItemsControl>

当然,如果您希望添加按钮首先出现,只需将Border(包含Button)的顺序与CompositeCollection 标记中的CollectionContainer 交换即可。

【讨论】:

这似乎会引发错误System.Windows.Data Error: 26 : ItemTemplate and ItemTemplateSelector are ignored for items already of the ItemsControl's container type; Type='Border',但在视觉上它似乎工作得很好

以上是关于将 WrapPanel 用作 ItemsPanel 的 ItemsControl - 结合“静态”子项和 ItemsSource的主要内容,如果未能解决你的问题,请参考以下文章

带有ScrollHeader和WrapPanel的ListView(ItemsPanel.ItemsPanelTemplate)

WPF - 如何在 WrapPanel 中居中所有项目?

WPF ListBox 横向排列

带有包装和虚拟化的ListBox

在 ListView 中动态调整图像大小

WPF实现竖向排列并换行显示