0

I am trying to build a custom ComboBox which displays a ListView. Here is a screen capture of what I am trying to achieve:

enter image description here

I based most of my code on this very helpful blog, which discusses how to embed a DataGrid (or in my case, a GridView), inside a ComboBox. From a functional point of view, everything is working. However, I can't seem to find a way to get the drop down to be fixed. Ideally, I would like it to appear as above always, regardless of the window size or window position. Currently, the popup tries to be right-aligned, except when the window gets close to the left edge of the screen, at which point the popup migrates inward. The problem, as the XAML below shows, is that the ListView sits inside a Popup, and Popups are not really bound to the normal window, and therefore we cannot directly control their position.

<Window.Resources>
  <Style x:Key="ComboBoxTest2" TargetType="{x:Type ComboBox}">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="ComboBox">
          <Grid>
            <ToggleButton x:Name="TGButton" Grid.Column="2" Focusable="false"
                          IsChecked="{Binding Path=IsDropDownOpen,Mode=TwoWay,
                          RelativeSource={RelativeSource TemplatedParent}}"
                          Padding="0,0,50,0">
              <ToggleButton.Template>
                <ControlTemplate>
                  <Grid>
                    <Grid.ColumnDefinitions>
                      <ColumnDefinition Width="*" />
                      <ColumnDefinition Width="30" />
                    </Grid.ColumnDefinitions>
                    <Border x:Name="Border" Grid.ColumnSpan="2" CornerRadius="5"
                         Background="LightGray" BorderBrush="Black" BorderThickness="1" />
                    <Border x:Name="Border2" Grid.Column="0" CornerRadius="5,0,0,5"
                            Margin="1" Background="White" BorderBrush="Black"
                            BorderThickness="0,0,1,0" />
                    <Path x:Name="Arrow" Grid.Column="1"  Fill="Black"
                          HorizontalAlignment="Center" VerticalAlignment="Center"
                          Data="M 0 0 L 4 4 L 8 0 Z"/>
                  </Grid>
                  <ControlTemplate.Triggers>
                    <Trigger Property="ToggleButton.IsMouseOver" Value="true">
                      <Setter TargetName="Border" Property="Background" Value="DarkGray" />
                    </Trigger>
                    <Trigger Property="ToggleButton.IsChecked" Value="true">
                      <Setter TargetName="Border" Property="Background" Value="DarkGray" />
                      <Setter TargetName="Border" Property="CornerRadius" Value="5,5,0,0" />
                      <Setter TargetName="Border2" Property="CornerRadius" Value="5,0,0,0" />
                    </Trigger>
                  </ControlTemplate.Triggers>
                </ControlTemplate>
              </ToggleButton.Template>
            </ToggleButton>
            <ContentPresenter Name="ContentSite" IsHitTestVisible="False"
                            Content="{TemplateBinding SelectionBoxItem}"
                            ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
                            ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
                            Margin="3,3,40,3" />
            <TextBox x:Name="PART_EditableTextBox" Visibility="Hidden"
                     IsReadOnly="{TemplateBinding IsReadOnly}" Width="50" />
            <Popup Name="Popup" IsOpen="{TemplateBinding IsDropDownOpen}"
                   AllowsTransparency="True" Focusable="False" PopupAnimation="Slide"
                   Placement="Relative" VerticalOffset="{TemplateBinding ActualHeight}"
                   HorizontalOffset="{TemplateBinding ActualWidth}">
             <Grid Name="DropDown" SnapsToDevicePixels="True"
                   MinWidth="{TemplateBinding ActualWidth}"
                   MaxHeight="{TemplateBinding MaxDropDownHeight}">
               <Border x:Name="DropDownBorder" Background="White" BorderThickness="1"
                       BorderBrush="Black"/>
                 <ListView ItemsSource="{TemplateBinding ItemsSource}"
                           SelectedItem="{Binding RelativeSource={RelativeSource TemplatedParent},
                           Path=SelectedItem}">
                   <ListView.View>
                     <GridView>
                       <GridViewColumn Header="Key" DisplayMemberBinding="{Binding Key}" />
                       <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" />
                     </GridView>
                   </ListView.View>
                 </ListView>
               </Grid>
             </Popup>
           </Grid>
         </ControlTemplate>
       </Setter.Value>
     </Setter>
   </Style>
 </Window.Resources>
<Grid>
  <ComboBox x:Name="cBox" Height="30" Width="200" ItemsSource="{Binding Path=People}"
            SelectedValue="Selected" DisplayMemberPath="Name" SelectedValuePath="Name"
            Style="{StaticResource ComboBoxTest2}" 
            SelectionChanged="Function_SelectionChanged" />
</Grid>

I have read in a few places that Adorners might be a solution, because they would receive all resize events and could reposition the popup dynamically. Another option might be to use a library like DevExpress, but I am trying to avoid this. By the way, my question is not a duplicate of this one, since the offsets used to place a Popup in XAML are only honored at rendering, not during resizing/moving.

Tim Biegeleisen
  • 502,043
  • 27
  • 286
  • 360

2 Answers2

1

Just change you popup placement options and you should be able to get what you want to display.

MainWindow.xaml

<Window x:Class="WpfApp1.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:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="300" Width="300">
    <Window.Resources>
        <Style x:Key="ComboBoxTest2" TargetType="{x:Type ComboBox}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ComboBox">
                        <Grid>
                            <ToggleButton x:Name="TGButton" Grid.Column="2" Focusable="false"
                          IsChecked="{Binding Path=IsDropDownOpen,Mode=TwoWay,
                          RelativeSource={RelativeSource TemplatedParent}}"
                          Padding="0,0,50,0">
                                <ToggleButton.Template>
                                    <ControlTemplate>
                                        <Grid>
                                            <Grid.ColumnDefinitions>
                                                <ColumnDefinition Width="*" />
                                                <ColumnDefinition Width="30" />
                                            </Grid.ColumnDefinitions>
                                            <Border x:Name="Border" Grid.ColumnSpan="2" CornerRadius="5"
                         Background="LightGray" BorderBrush="Black" BorderThickness="1" />
                                            <Border x:Name="Border2" Grid.Column="0" CornerRadius="5,0,0,5"
                            Margin="1" Background="White" BorderBrush="Black"
                            BorderThickness="0,0,1,0" />
                                            <Path x:Name="Arrow" Grid.Column="1"  Fill="Black"
                          HorizontalAlignment="Center" VerticalAlignment="Center"
                          Data="M 0 0 L 4 4 L 8 0 Z"/>
                                        </Grid>
                                        <ControlTemplate.Triggers>
                                            <Trigger Property="ToggleButton.IsMouseOver" Value="true">
                                                <Setter TargetName="Border" Property="Background" Value="DarkGray" />
                                            </Trigger>
                                            <Trigger Property="ToggleButton.IsChecked" Value="true">
                                                <Setter TargetName="Border" Property="Background" Value="DarkGray" />
                                                <Setter TargetName="Border" Property="CornerRadius" Value="5,5,0,0" />
                                                <Setter TargetName="Border2" Property="CornerRadius" Value="5,0,0,0" />
                                            </Trigger>
                                        </ControlTemplate.Triggers>
                                    </ControlTemplate>
                                </ToggleButton.Template>
                            </ToggleButton>
                            <ContentPresenter Name="ContentSite" IsHitTestVisible="False"
                            Content="{TemplateBinding SelectionBoxItem}"
                            ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
                            ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
                            Margin="3,3,40,3" />
                            <TextBox x:Name="PART_EditableTextBox" Visibility="Hidden"
                     IsReadOnly="{TemplateBinding IsReadOnly}" Width="50" />
                            <Popup Name="Popup" IsOpen="{TemplateBinding IsDropDownOpen}"
                   AllowsTransparency="True" Focusable="False" PopupAnimation="Slide"
                   Placement="Bottom">
                                <Grid Name="DropDown" SnapsToDevicePixels="True"
                   MinWidth="{TemplateBinding ActualWidth}"
                   MaxHeight="{TemplateBinding MaxDropDownHeight}">
                                    <Border x:Name="DropDownBorder" Background="White" BorderThickness="1"
                       BorderBrush="Black"/>
                                    <ListView ItemsSource="{TemplateBinding ItemsSource}"
                           SelectedItem="{Binding RelativeSource={RelativeSource TemplatedParent},
                           Path=SelectedItem}">
                                        <ListView.View>
                                            <GridView>
                                                <GridViewColumn Header="Key" DisplayMemberBinding="{Binding Key}" Width="80" />
                                                <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" Width="250"/>
                                            </GridView>
                                        </ListView.View>
                                    </ListView>
                                </Grid>
                            </Popup>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Grid>
        <ComboBox x:Name="cBox" Height="30" Width="200" ItemsSource="{Binding}"
            SelectedValue="Selected" DisplayMemberPath="Name" SelectedValuePath="Name"
            Style="{StaticResource ComboBoxTest2}" 
            SelectionChanged="Function_SelectionChanged" />
    </Grid>
</Window>

MainWindow.xaml.cs

using System.Linq;
using System.Windows;
using System.Windows.Controls;

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = Enumerable.Range(1, 100).Select(x => new { Key = x, Name = $"Person {x} with really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really long name" }).ToList();
        }

        private void Function_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            // do whatever you want
        }
    }
}

Output:

enter image description here

Dipen Shah
  • 25,562
  • 1
  • 32
  • 58
  • 1
    This places the drop down on the bottom (correct), but it is right aligned. Do you know how to force the popup to be aligned with the left side of the ComboBox? – Tim Biegeleisen Aug 01 '18 at 04:37
  • @TimBiegeleisen Not sure what do you mean, it is already aligned with left side of the combobox. – Dipen Shah Aug 01 '18 at 04:38
  • That screen capture is what I _want_, not the current state. WPF's default behavior is to right align. – Tim Biegeleisen Aug 01 '18 at 04:39
  • I appreciate the help, but you couldn't have used my XAML code, or else you would see the same thing I am seeing. – Tim Biegeleisen Aug 01 '18 at 04:47
  • @TimBiegeleisen Let me put entire code. Does it seem familiar? – Dipen Shah Aug 01 '18 at 04:48
  • I think the issue may be your data. You only have `Pension xxx` as data, whereas I have a really long entry in there. Also, what happens if you try opening the ComboBox when the drop down (arrow) button is flush with the right side of the screen? Does it still render left aligned? – Tim Biegeleisen Aug 01 '18 at 04:52
  • @TimBiegeleisen Checkout my latest edit and if it still doesn't work for you then may be you should share your code on github otherwise AFAIK no body will be able to replicate your issue. – Dipen Shah Aug 01 '18 at 04:59
  • Weird...even with your latest code my box is still right aligned. Maybe it's a .NET version issue then. – Tim Biegeleisen Aug 01 '18 at 05:03
  • @TimBiegeleisen Try setting FlowDirection="LeftToRight" on the combobox. – Dipen Shah Aug 01 '18 at 05:05
0

ComboBox underhood combines TextBox and a Popup. This message is self-explanatory:

Depending on your Windows settings related to handedness, the popup may be left or right-aligned when shown on the top or bottom.

Source Link