Thursday, January 23, 2020

WPF Multi-column dropdown list

My users want a dropdown list that can display multiple columns. In addition they want to be able to disabled selected items in the dropdown list.

We started with the Infragistics XamMultiColumnComboEditor which isn't very satisfactory. The first things the designers don't like is that when you collapse it, you only see one column. They also don't like that you cannot turn the filter feature off. Here are a couple of screen prints showing these issues.

XamMultiColumnComboEditor shows all three columns when expanded...
but only one when collapsed.
The filter feature works well if you have a large number of items, but our designers worry it will confuse our users so they want it turned off. I've researched this and there isn't a way to turn it off.

Really need to be able to turn the filter feature off.
So our designers want a simple drop down box that displays multiple columns when expanded and when collapsed and also has the ability to disable entries.

We can override the default ItemTemplate for a ComboBox in XAML and use a style to disable selected rows. I came up with two UI designs, one with the column labels above the Combobox and one with them embedded using a converter. Let's take a look at both methods.

Note these designs are ideal for single use, but a custom control would be better when this functionality will be wide-spread. That's for another post.

Start a new WPF project called MultiColumnCombo using C# and any version of the Framework after 3.5.

The XAML (MainWindow.xaml) looks like this. It shows the two drop down lists described above.

<Window x:Class="MultiColumnCombo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:MultiColumnCombo"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <local:PromptConverter x:Key="PromptConverter"/>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="100"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Grid Grid.Row="0">
            <Grid.RowDefinitions>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
            </Grid.RowDefinitions>
            <StackPanel Grid.Row="0" Orientation="Horizontal">
                <TextBlock Text="Description" Margin="8,0,2,0" Width="200"></TextBlock>
                <TextBlock Text="Setup" Margin="2,0,0,0" Width="35"></TextBlock>
                <TextBlock Text="Post" Margin="2,0,0,0" Width="35"></TextBlock>
            </StackPanel>
            <ComboBox Grid.Row="1"  HorizontalAlignment="Left" ItemsSource="{Binding RollTypes}" Width="300" SelectedValuePath="ID" SelectedItem="{Binding SelectedRollType}" Height="22">
                <ComboBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding Description}" Margin="2,0,2,0" Width="200"/>
                            <TextBlock Text="{Binding SetupObject}" Margin="2,0,0,0" Width="35"/>
                            <TextBlock Text="{Binding PostObject}" Margin="2,0,0,0" Width="35"/>
                        </StackPanel>
                    </DataTemplate>
                </ComboBox.ItemTemplate>
                <ComboBox.ItemContainerStyle>
                    <Style TargetType="ComboBoxItem">
                        <Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
                    </Style>
                </ComboBox.ItemContainerStyle>
            </ComboBox>
        </Grid>
        <ComboBox Grid.Row="2" HorizontalAlignment="Left" ItemsSource="{Binding RollTypes}" Width="380" SelectedValuePath="ID" SelectedItem="{Binding SelectedRollType}" Height="22">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Description}" Margin="2,0,2,0" Width="200"/>
                        <TextBlock Text="{Binding SetupObject, Converter={StaticResource PromptConverter}, ConverterParameter='Setup: '}" Margin="2,0,0,0" Width="80"/>
                        <TextBlock Text="{Binding PostObject, Converter={StaticResource PromptConverter}, ConverterParameter='Post: '}" Margin="2,0,0,0" Width="80"/>
                    </StackPanel>
                </DataTemplate>
            </ComboBox.ItemTemplate>
            <ComboBox.ItemContainerStyle>
                <Style TargetType="ComboBoxItem">
                    <Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
                </Style>
            </ComboBox.ItemContainerStyle>
        </ComboBox>
    </Grid>
</Window>

 The code behind (MainWindow.xaml.cs) looks like this. It simply defines a list of objects we can bind to.

using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;

namespace MultiColumnCombo
{
    public class cRollType
    {
        public int ID { get; set; }
        public string Description { get; set; }
        public string SetupObject { get; set; }
        public string PostObject { get; set; }
        public bool IsEnabled { get; set; }
    }

    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public List<cRollType> RollTypes { get; set; }
        public cRollType SelectedRollType { get; set; }
        public MainWindow()
        {
            RollTypes = new List<cRollType>();
            RollTypes.Add(new cRollType() { ID = 0, Description = "(SELECT)", SetupObject = "", PostObject = "", IsEnabled = false });
            RollTypes.Add(new cRollType() { ID = 1, Description = "Accounts Receivable", SetupObject = "9229", PostObject = "9210", IsEnabled = true });
            RollTypes.Add(new cRollType() { ID = 2, Description = "Due from grantor government", SetupObject = "9299", PostObject = "9290", IsEnabled = false });
            RollTypes.Add(new cRollType() { ID = 3, Description = "Due from other funds", SetupObject = "9319", PostObject = "9310", IsEnabled = true });
            SelectedRollType = RollTypes[0];
            InitializeComponent();
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public void PropChanged(string name)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }
}


I wrote a simple converter to handle the column prompts so that we could suppress them for the (SELECT) item.

using System;
using System.Globalization;
using System.Windows.Data;

namespace MultiColumnCombo
{
    class PromptConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null || value.ToString() == "")
                return "";
            else
                return parameter.ToString() + value.ToString();
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

The UI for the first list box (labels above the box) looks like this.
Initial collapsed state
Expanded state - note the disabled item
After selection

The UI for the second list box (labels embedded in the items) looks like this.

Initial collapsed state
Expanded state - note labels
After selection






No comments:

Post a Comment