Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Multi-select functionality for ComboBox #263

Open
SavoySchuler opened this issue Feb 4, 2019 · 13 comments
Open

Proposal: Multi-select functionality for ComboBox #263

SavoySchuler opened this issue Feb 4, 2019 · 13 comments
Assignees
Labels
area-ComboBox feature proposal New feature proposal team-Controls Issue for the Controls team

Comments

@SavoySchuler
Copy link
Member

SavoySchuler commented Feb 4, 2019

The WinUI Team has opened a Spec for this feature

Proposal: Multi-select functionality for ComboBox

Summary

This feature would add multi-select capabilities in the dropdown of ComboBox to enable group selection/filtering in space conservative scenarios.

closed combobox that reads "Red, Blue, Gree..." in the preview before being cut off by the dropdown button

combobox with a tick mark to the left of its only selected item

Rationale

Multi-select support for ComboBox is a regular request from enterprise developers and MVPs. Several third-party solutions exist for enabling multi-select functionality on WPF’s ComboBox. Implementing this feature in parallel to Grouping will delight developers seeking a fully featured ComboBox in UWP.

Functional Requirements

# Feature Priority
1 Multiple ComboBox items can be selected. Must
2 Multi-select status is indicated in the collapsed ComboBox display. Must
3 Drop down does not close when item gets selected. Must
4 Select All/Deselect All box at top of list. Should
5 Can select/de-select items by group. Dependent on Grouping. Should

Important Notes

Open Questions

@mdtauk
Copy link
Contributor

mdtauk commented Feb 5, 2019

Should the entries have a complete Checkbox as in the Checkbox Control, or a simplified tick mark as with the Menu and Context Menu controls?

@SavoySchuler
Copy link
Member Author

@ChainReactive Would you mind sharing how you achieved your solution?

@HappyNomad
Copy link

@SavoySchuler I'm happy to share how my multiselect scenario is working, apart from grouping in the UI which is not (waiting on #33).

In IngredientTemplateSelector, you'll notice the properties MinimumSelection and MaximumSelection. My MVVM framework (soon to be open-sourced) has an abstract SelectableNode class that can manage a wide variety of single and multi selection scenarios.

You'll also notice that each group in my scenario consists of either check boxes or radio buttons. It varies by group. For this reason, please provide a way to replace the check box with a radio button (via a data template i guess) either for the whole group or individually.

Local XAML resources:

<Grid.Resources>
    <CollectionViewSource x:Key="ingredients" Source="{Binding ItemData.Ingredients}" IsSourceGrouped="True" ItemsPath="SubItems"/>
    <DataTemplate x:Key="checkbox">
        <CheckBox Content="{Binding DomainItem.Type.Phrase}" IsChecked="{Binding IsSelected, Mode=TwoWay}" Foreground="Black" Padding="10"/>
    </DataTemplate>
    <DataTemplate x:Key="radiobutton">
        <RadioButton Content="{Binding DomainItem.Type.Phrase}" IsChecked="{Binding IsSelected, Mode=TwoWay}" Foreground="Black" Padding="10"
                     GroupName="{Binding Parent}"/>
    </DataTemplate>
    <ui:IngredientTemplateSelector x:Key="ingredientTemplateSelector"
          CheckBoxTemplate="{StaticResource checkbox}" RadioButtonTemplate="{StaticResource radiobutton}"/>
</Grid.Resources>

The combo box:

<lib:ComboBoxAdorner Grid.Column="2" Text="{Binding ItemData.IngredientChanges}" HorizontalAlignment="Left" Margin="15,0,0,0">
    <ComboBox ItemsSource="{Binding Source={StaticResource ingredients}}"
              ItemTemplateSelector="{StaticResource ingredientTemplateSelector}">
        <!-- TODO: Uncomment below once grouping is possible in ComboBox -->
        <!--<ComboBox.GroupStyle>
            <GroupStyle>
                <GroupStyle.HeaderTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding}"/>
                    </DataTemplate>
                </GroupStyle.HeaderTemplate>
            </GroupStyle>
        </ComboBox.GroupStyle>-->
        <ComboBox.ItemContainerStyle>
            <Style TargetType="ComboBoxItem">
                <!-- Increase the "hitability" of the contained checkboxes/radio buttons -->
                <Setter Property="Padding" Value="0"/>
                <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
                <Setter Property="VerticalContentAlignment" Value="Stretch"/>
            </Style>
        </ComboBox.ItemContainerStyle>
    </ComboBox>
</lib:ComboBoxAdorner>

The template selector:

public class IngredientTemplateSelector : DataTemplateSelector
{
    public DataTemplate RadioButtonTemplate { get; set; }
    public DataTemplate CheckBoxTemplate { get; set; }

    protected override DataTemplate SelectTemplateCore( object item, DependencyObject container )
    {
        if ( item == null ) return null;

        var ingredientPM = (OptionPM)item;
        var ingredientsListPM = ingredientPM.Parent;
        return ingredientsListPM.RootData.MinimumSelection == 1 && ingredientsListPM.RootData.MaximumSelection == 1 ?
            RadioButtonTemplate : CheckBoxTemplate;
    }
}

The combo box adorner:

[TemplateVisualState( Name = "Normal", GroupName = "CommonStates" )]
[TemplateVisualState( Name = "Disabled", GroupName = "CommonStates" )]
public class ComboBoxAdorner : ContentControl
{
    public ComboBoxAdorner()
    {
        DefaultStyleKey = typeof( ComboBoxAdorner );
        IsEnabledChanged += this_IsEnabledChanged;
    }

    #region 'Text' Identifier

    public string Text
    {
        get { return (string)GetValue( TextProperty ); }
        set { SetValue( TextProperty, value ); }
    }

    public static readonly DependencyProperty TextProperty =
        DependencyProperty.Register(
            "Text", typeof( string ), typeof( ComboBoxAdorner ), null
        );

    #endregion 'Text' Identifier

    #region Event Handlers

    protected override void OnApplyTemplate()
    {
        VisualStateManager.GoToState( this, IsEnabled ? "Normal" : "Disabled", false );
        base.OnApplyTemplate();
    }

    void this_IsEnabledChanged( object sender, DependencyPropertyChangedEventArgs e )
    {
        VisualStateManager.GoToState( this, (bool)e.NewValue ? "Normal" : "Disabled", true );
    }

    protected override void OnContentChanged( object oldContent, object newContent )
    {
        if ( oldContent != null ) {
            var comboBox = (ComboBox)oldContent;
            comboBox.SelectionChanged -= comboBox_SelectionChanged;

            comboBox.ClearValue( ComboBox.VerticalAlignmentProperty );
            comboBox.ClearValue( ComboBox.HorizontalAlignmentProperty );
            if ( comboBox.HorizontalAlignment != originalHorizontalContentAlignment )
                comboBox.HorizontalAlignment = originalHorizontalContentAlignment;
            if ( comboBox.VerticalAlignment != originalVerticalContentAlignment )
                comboBox.VerticalAlignment = originalVerticalContentAlignment;
        }

        if ( newContent != null ) {
            var comboBox = newContent as ComboBox;
            if ( comboBox == null )
                throw new InvalidOperationException( "ComboBoxAdorner must contain a ComboBox" );

            originalHorizontalContentAlignment = comboBox.HorizontalAlignment;
            originalVerticalContentAlignment = comboBox.VerticalAlignment;
            comboBox.HorizontalAlignment = HorizontalAlignment.Stretch;
            comboBox.VerticalAlignment = VerticalAlignment.Stretch;

            comboBox.SelectionChanged += comboBox_SelectionChanged;
        }

        base.OnContentChanged( oldContent, newContent );
    }
    HorizontalAlignment originalHorizontalContentAlignment;
    VerticalAlignment originalVerticalContentAlignment;

    // Prevent ComboBox selection, which would be meaningless.
    void comboBox_SelectionChanged( object sender, SelectionChangedEventArgs e )
    {
        if ( e.AddedItems.Count > 0 )
            ( (ComboBox)sender ).SelectedItem = null;
    }

    #endregion Event Handlers
}

The combo box adorner's XAML:

<Style TargetType="local:ComboBoxAdorner">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:ComboBoxAdorner">
                <Grid>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Normal" />
                            <VisualState x:Name="Disabled">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="textBlock" Storyboard.TargetProperty="Foreground">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ComboBoxDisabledForegroundThemeBrush}" />
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <ContentPresenter Name="comboBoxPresenter"/>
                    <TextBlock Name="textBlock" Text="{TemplateBinding Text}" TextWrapping="Wrap"
                               Foreground="{ThemeResource ComboBoxForeground}" IsHitTestVisible="False" Margin="12,5,32,7">
                        <!-- Prevent issues while selecting/deselecting ingredients (growing/shrinking of popup
                             and jumping to middle of list) -->
                        <TextBlock.Visibility>
                            <Binding Path="Content.IsDropDownOpen" ElementName="comboBoxPresenter">
                                <Binding.Converter>
                                    <local:BooleanConverter TrueValue="Collapsed" FalseValue="Visible"/>
                                </Binding.Converter>
                            </Binding>
                        </TextBlock.Visibility>
                    </TextBlock>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

@MikeHillberg
Copy link
Contributor

MikeHillberg commented Mar 18, 2019

What does the closed state look like? Could this be a DropDownButton with a ListView?

@SavoySchuler
Copy link
Member Author

@MikeHillberg I added an image of what I was imagining to the summary. Please let me iknow what you think!

@SavoySchuler
Copy link
Member Author

@mdtauk I believe you are correct, I have updated it.

@SavoySchuler
Copy link
Member Author

@ChainReactive, this is awesome! Thank you for sharing this with us! It's clear that we have room to make this more easily achievable and your work is an excellent starting point for figuring out how.

@SavoySchuler
Copy link
Member Author

@mdtauk and @ChainReactive, thank you both for also helping to getting this feature started!

It has been approved and I have opened up a spec for it here.

As noted on Grouping Support for ComboBox, we would be eager to see you involved in our spec writing where you can tell us specifics about how you would like this feature implemented. @niels9001, you may also be interested since this feature development will be cooperative with the Grouping Support for ComboBox you pitched.

It may be several months before we are able to fully commit PM & Dev resources to this feature, but your early engagement will still help jumpstart both of these developments. Please let me know if you have any questions. I have added our default spec template and will jump into contribute when I can!

@HappyNomad
Copy link

What does the closed state look like?

@SavoySchuler The image in the summary looks fine as a default, but it won't suffice in my scenario. I expect I'll be able to continue binding the Text property to my view-model.

@jevansaks jevansaks added the team-Controls Issue for the Controls team label Nov 7, 2019
@niels9001
Copy link
Contributor

@SavoySchuler It's been a while since this thread was opened. I see that grouping for the ComboBox would require WinUI 3.0.

Are there any updates on this topic? Anything we can do to speed up the progress?

@jamesmcroft
Copy link
Member

Brought this up in the questions of today's session and wondered if it had already been requested. Is this something that we might see come in WinUI 3 or potentially post RTM @SavoySchuler ?

I've previously built a custom control which provides ComboBox-like support for multi select taking advantage of the UWP ListView control but it would be awesome to see this done natively instead.

@jamesmcroft
Copy link
Member

Update on this since the last comment made, I decided to publish my ComboBox-like control that supports both single and multiple selection modes in the interim while this functionality is not available

https://made-apps.github.io/MADE.NET/articles/features/ui-controls-dropdownlist.html

@KWodarczyk
Copy link

Maybe it would be useful to add a button at the bottom of the list e.g. "Apply" so that user can apply changes while combo box is open ? This way we also save some space in the app as the button is only needed while selecting from combobox. JIRA does something like this:

Image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-ComboBox feature proposal New feature proposal team-Controls Issue for the Controls team
Projects
None yet
Development

No branches or pull requests

8 participants