Search code examples
c#wpfxamllistboxdatatemplate

How to have a custom display in ListBox with DataTemplate?


I need to display cards in a ListBox in a specific layout:

https://i.sstatic.net/csDSL.jpg

I've tried to find a way to use 2 types of DataTemplate but I have no idea how to do it. I decided to make a template which contains 6 card Template (like this):

https://i.sstatic.net/J0C3l.jpg

Here's what my current Template looks like:

<ControlTemplate x:Key = "CardTemplate" TargetType = "Button">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="4*"/>
            <RowDefinition Height="1*"/>
        </Grid.RowDefinitions>
        <Image Grid.Row="0" Source="{Binding Path=Image}"/>
        <TextBlock Grid.Row="1" Text="{Binding Path=Titre}"/>
    </Grid>
</ControlTemplate>

<DataTemplate x:Key="DataTemplate">
    <Grid>

        <Grid.RowDefinitions>
            <RowDefinition Height="1*"/>
            <RowDefinition Height="1*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="1*"/>
        </Grid.ColumnDefinitions>

        <Button Grid.Column="0" Grid.Row="0" Template="{StaticResource CardTemplate}"/>

        <Grid Grid.Column="1" Grid.Row="0">
            <Grid.RowDefinitions>
                <RowDefinition Height="1*"/>
                <RowDefinition Height="1*"/>
            </Grid.RowDefinitions>
            <Button Grid.Row="0" Template="{StaticResource CardTemplate}"/>
            <Button Grid.Row="1" Template="{StaticResource CardTemplate}"/>
        </Grid>

        <Grid Grid.Column="0" Grid.Row="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="1*"/>
                <RowDefinition Height="1*"/>
            </Grid.RowDefinitions>
            <Button Grid.Row="0" Template="{StaticResource CardTemplate}"/>
            <Button Grid.Row="1" Template="{StaticResource CardTemplate}"/>
        </Grid>

        <Button Grid.Column="1" Grid.Row="1" Template="{StaticResource CardTemplate}"/>

    </Grid>
</DataTemplate>

Which I intend to display in a ListBox:

<ListBox ScrollViewer.HorizontalScrollBarVisibility="Disabled"
         Name="ListBox" ItemTemplate="{DynamicResource DataTemplate}" 
         ScrollBar.Scroll="ScrollOnBottom">
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel/>
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
</ListBox>

Here's how my cod behind basically works:

class Cards
{
    public List<Card> cards; // list of 6 Card objects
}

class Card
{
    string title;
    BitmapImage image;

    public string Title { get => title; set => title = value; }
    public BitmapImage Image { get => image; set => image = value; }
}

ObservableCollection<Cards> cc = new ObservableCollection<Cards>();
/*

Cards are already filled with 6 Card
cc filled with Cards

*/
formationListBox.ItemsSource = cc;

Here's the problem, it displays the right amount of Cards but the buttons are empty. I don't know how to bind a specific object to each button.


Solution

  • To Give an example of what Sinatr commented. You should approach this from an Mvvm perspective. first you should add a View Model for the View that this Window is in. This will contain a list of objects that are DisplayCards each object will store the string and image.

    public class DisplayCard : INotifyPropertyChanged
    {
        private string _title;
        public string Title
        {
            get { return _title; }
            set
            {
                if (value != _title) { _title = value; RaisePropertyChanged(); }
            }
        }
        private string _cardImage;
        public string CardImage
        {
            get { return _cardImage; }
            set
            {
                if (value != _cardImage) { _cardImage = value; RaisePropertyChanged(); }
            }
        }
        public event PropertyChangedEventHandler PropertyChanged;
        protected void RaisePropertyChanged([CallerMemberName] string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    public class YourViewVM : INotifyPropertyChanged
    {
        private ObservableCollection<DisplayCard> _cardCollection;
        public ObservableCollection<DisplayCard> CardCollection
        {
            get { return _cardCollection; }
            set
            {
                if (value != _cardCollection) { _cardCollection = value; RaisePropertyChanged(); }
            }
        }
        public event PropertyChangedEventHandler PropertyChanged;
        protected void RaisePropertyChanged([CallerMemberName] string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    

    Then you need to make the list CardCollection set to the ListBox's ItemSource. Then use a datatemplate to bind the DisplayCards properties to the containing object.

    <ListBox Name="lbTodoList" HorizontalContentAlignment="Stretch" ItemSource="{Binding CardCollection}">
      <ListBox.ItemTemplate>
        <DataTemplate>
          <Grid>
            <Grid.RowDefinitions>
              <RowDefinition Height="4*"/>
              <RowDefinition Height="1*"/>
            </Grid.RowDefinitions>
            <Image Grid.Row="0" Source="{Binding Image}"/>
            <TextBlock Grid.Row="1" Text="{Binding Title}"/>
          </Grid>
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>
    

    You need to make sure you set the YourViewVM As the DataContext of the View. A simple search should solve how to do that.

    The above should be enough to allow you to refactor your code such that it works.