Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ItemsControl with WrapPanel as ItemsPanel - combine a "static" child and ItemsSource

Tags:

c#

wpf

xaml

Using an ItemsControl with a WrapPanel set as the ItemsPanel, I am trying to achieve what is illustrated in this image: ItemsControl with WrapPanel as ItemsPanel

The XAML looks like this (modified to make it more simple):

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

The underlying ViewModel looks like this:

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

The ItemsControl's DataContext is set to an instance of Zoo and populated with 4 Animals.

The question: How can I add a "static" child element that looks like the other elements, is a child of the WrapPanel, and wraps with the other children? Specifically, I want the last element to be an add-button (the green plus-sign shown in the image above), which is bound to the AddAnimal Command property of the Zoo.

Requirements:

  • The add-button-element must wrap with the other children of the WrapPanel.
  • The add-button-element must always be the last (or first) element and should also be visible if there are no Animals in the Zoo.
  • Adding a dummy Animal to the Animals-collection and then use a style to modify the last child is not an option. The underlying model is used a lot of places in the application and thus it is too hackish to have a dummy Animal floating around in the Animals-collection.

I have thought about:

  • Using a converter that copies the Animals-collection into a new collection, adds a dummy Animal to the copy, and returns it to the ItemsControl's ItemsSource binding. Then I could style the last element as an add-button. However, this is not an option as some drag-and-drop logic needs to be able to work on the original ObservableCollection via the ItemsControl's ItemsSource property.
  • Sub-classing the WrapPanel and adding the add-button-element as a child element without modifying the ItemsSource property seems like the best option (if possible), but I cannot figure out how to do that.

Additional information: I use: WPF, PRISM, C# 6.0, .NET 4.0 Client Profile and the MVVM pattern.

Any thoughts on this problem?

Solution:

@kyriacos_k's answers solved it for me with two minor modification:

  1. It didn't work if the DataTemplate was set via the ItemsControl.ItemTemplate property, i.e. the Add-button would pick up the DataTemplate and get displayed incorrectly. I guess this is by design, as MSDN states: "An ItemsControl uses the data in the CompositeCollection to generate its content according to its ItemTemplate", source: https://msdn.microsoft.com/en-us/library/system.windows.data.compositecollection%28v=vs.110%29.aspx)
  2. I had to use a "binding proxy" to get the AddAnimal Command binding to work. Neither "direct" binding syntax or relative source worked. I guess it is because the add button is not part of the same visual tree and somehow it doesn't pickup the DataContext from the ItemsControl.

In the end this is what worked for me:

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

The code for the BindingProxy is here (snatched directly from: 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));
}
like image 885
sb.olofsson Avatar asked Oct 28 '15 08:10

sb.olofsson


2 Answers

You can do it in a very neat way, using the 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>

Of course, if you want the add button to appear first, just swap the order of the Border (containing the Button) with the CollectionContainer in the CompositeCollection tag.

like image 113
kkyr Avatar answered Nov 15 '22 00:11

kkyr


Take a look at CompositeCollection. It lets you add additional items to an ItemsSource binding:

<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>
like image 39
andreask Avatar answered Nov 15 '22 00:11

andreask