Search code examples
wpfxamllayoutlistbox

ListBox Split Into Two Columns


I would like to have my ListBox layout like this: A tabular layout with two columns, one for the title and one for the authors of books.

But currently I have this: A single column showing data.

And my xml code looks like this:

<ListBox DockPanel.Dock="Top" Grid.Column="1" Grid.Row="1" Height="200" ScrollViewer.VerticalScrollBarVisibility="Visible">
    <ListBoxItem Width="325" Content="Visual Basic"/>
    <ListBoxItem Width="325" Content="Silverlight"/>
    <ListBoxItem Width="325" Content="ASP.NET"/>
    <ListBoxItem Width="325" Content="WCF"/>
    <ListBoxItem Width="325" Content="Web Services"/>
    <ListBoxItem Width="325" Content="Windows Service"/>
</ListBox>

So how can I make two columns in a ListBox and add data to them?


Solution

  • Here is an inspiration how you could display your enthusiasm for boats. This is a simple example in code-behind, as I do not know how deep your knowledge about WPF is.

    First, create a data type that represents a book with a Title and an Authors property.

    public class Book
    {
       public Book(string title, IEnumerable<string> authors)
       {
          Title = title;
          Authors = authors;
       }
    
       public string Title { get; }
    
       public IEnumerable<string> Authors { get; }
    }
    

    Then in your window's code-behind, expose a collection for your books and initialize it in the constructor. Set the DataContext to this, so bindings will have the window as source and find the Books property.

    public partial class MainWindow
    {
       public MainWindow()
       {
          InitializeComponent();
          
          Books = new List<Book>
          {
             new Book("Boaty McBoatface", new[] { "Byron Barton" }),
             new Book("Boat life", new[] { "Jeremy Boats", "Bud Shipman" }),
             new Book("Boat Boys", new[] { "Will Buy Boats" }),
          };
          
          DataContext = this;
       }
       
       public IEnumerable<Book> Books { get; }
    }
    

    Now, create a ListView that binds its ItemsSource to the Books property. Remember, the binding will resolve the property on the window, as we set it as DataContext.

    Add a GridView to the View property of the ListView. A GridView is responsible for displaying the columns, which you have to define. A GridViewColumn has a DisplayMemberBinding which can be bound to any property within the item type of the bound collection, here Book. If you bind a property like this, the value will be displayed as plain text in a column cell. For custom data types, you can specify a DataTemplate as CellTemplate.

    <ListView ItemsSource="{Binding Books}">
       <ListView.Resources>
          <local:StringsToCommaSeparatedStringConverter x:Key="StringsToCommaSeparatedStringConverter"/>
       </ListView.Resources>
       <ListView.View>
          <GridView>
             <GridViewColumn Header="Title"
                             DisplayMemberBinding="{Binding Title}"/>
             <GridViewColumn Header="Authors"
                             DisplayMemberBinding="{Binding Authors, Converter={StaticResource StringsToCommaSeparatedStringConverter}}"/>
          </GridView>
       </ListView.View>
    </ListView>
    

    For the collection of authors we use another techique in WPF, value converters. Those can be used in binding and will convert a value from the source to something else for the target to display and vice-versa. Here, I created a custom value converter, that converts a collection of strings to a comma-separted single string.

    public class StringsToCommaSeparatedStringConverter : IValueConverter
    {
       public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
       {
          if (!(value is IEnumerable<string> values))
             return Binding.DoNothing;
    
          return string.Join(", ", values);
       }
    
       public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
       {
          throw new InvalidOperationException("This converter is not implemented two-way, because of lazyness.");
       }
    }
    

    As you saw in the XAML above, you need to create an instance of the converter in any resources in scope and specify it in the binding where you want to apply it.

    A GridView with two columns, one for the book title and one for authors.


    This is a more or less simple example to start with. You should sooner or later start learning the MVVM design pattern to improve separation of the user interface and your data and business logic. To migrate this example to MVVM, you would move the books collection property to a view model that you would set as DataContext e.g. of the window.

    Further reading:

    By the way, in general a ListBox does not have built-in support for columns, a ListView does through GridView. There is even a much more powerful but at the same time complex control for tabular data, the DataGrid, in case you ever need it. Keep on boating!