I know how to do it in code, but can this be done in XAML ?
Window1.xaml:
Alun Harford's approach, in practice :
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- hidden listbox that has all the items in one grid -->
<ListBox ItemsSource="{Binding Items, ElementName=uiComboBox, Mode=OneWay}" Height="10" VerticalAlignment="Top" Visibility="Hidden">
<ListBox.ItemsPanel><ItemsPanelTemplate><Grid/></ItemsPanelTemplate></ListBox.ItemsPanel>
</ListBox>
<ComboBox VerticalAlignment="Top" SelectedIndex="0" x:Name="uiComboBox">
<ComboBoxItem>foo</ComboBoxItem>
<ComboBoxItem>bar</ComboBoxItem>
<ComboBoxItem>fiuafiouhoiruhslkfhalsjfhalhflasdkf</ComboBoxItem>
</ComboBox>
</Grid>
This keeps the width to the widest element but only after opening the combo box once.
<ComboBox ItemsSource="{Binding ComboBoxItems}" Grid.IsSharedSizeScope="True" HorizontalAlignment="Left">
<ComboBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="sharedSizeGroup"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding}"/>
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Put an listbox containing the same content behind the dropbox. Then enforce correct height with some binding like this:
<Grid>
<ListBox x:Name="listBox" Height="{Binding ElementName=dropBox, Path=DesiredSize.Height}" />
<ComboBox x:Name="dropBox" />
</Grid>
I wanted it to only resize to the max element while the dropdown is open, and otherwise fit to the selected value. Here's the code for that:
Based in part on Frederik's answer (which didn't actually work for me)
public static class ComboBoxAutoWidthBehavior {
public static readonly DependencyProperty ComboBoxAutoWidthProperty =
DependencyProperty.RegisterAttached(
"ComboBoxAutoWidth",
typeof(bool),
typeof(ComboBoxAutoWidthBehavior),
new UIPropertyMetadata(false, OnComboBoxAutoWidthPropertyChanged)
);
public static bool GetComboBoxAutoWidth(DependencyObject obj) {
return (bool) obj.GetValue(ComboBoxAutoWidthProperty);
}
public static void SetComboBoxAutoWidth(DependencyObject obj, bool value) {
obj.SetValue(ComboBoxAutoWidthProperty, value);
}
private static void OnComboBoxAutoWidthPropertyChanged(DependencyObject dpo, DependencyPropertyChangedEventArgs e) {
if(dpo is ComboBox comboBox) {
if((bool) e.NewValue) {
comboBox.Loaded += OnComboBoxLoaded;
comboBox.DropDownOpened += OnComboBoxOpened;
comboBox.DropDownClosed += OnComboBoxClosed;
} else {
comboBox.Loaded -= OnComboBoxLoaded;
comboBox.DropDownOpened -= OnComboBoxOpened;
comboBox.DropDownClosed -= OnComboBoxClosed;
}
}
}
private static void OnComboBoxLoaded(object sender, EventArgs eventArgs) {
ComboBox comboBox = (ComboBox) sender;
comboBox.SetMaxWidthFromItems();
}
private static void OnComboBoxOpened(object sender, EventArgs eventArgs) {
ComboBox comboBox = (ComboBox) sender;
comboBox.Width = comboBox.MaxWidth;
}
private static void OnComboBoxClosed(object sender, EventArgs eventArgs) => ((ComboBox) sender).Width = double.NaN;
}
public static class ComboBoxExtensionMethods {
public static void SetMaxWidthFromItems(this ComboBox combo) {
double idealWidth = combo.MinWidth;
string longestItem = combo.Items.Cast<object>().Select(x => x.ToString()).Max(x => (x?.Length, x)).x;
if(longestItem != null && longestItem.Length >= 0) {
string tmpTxt = combo.Text;
combo.Text = longestItem;
Thickness tmpMarg = combo.Margin;
combo.Margin = new Thickness(0);
combo.UpdateLayout();
combo.Width = double.NaN;
combo.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
idealWidth = Math.Max(idealWidth, combo.DesiredSize.Width);
combo.Text = tmpTxt;
combo.Margin = tmpMarg;
}
combo.MaxWidth = idealWidth;
}
}
And you enable it like this:
<ComboBox behaviours:ComboBoxAutoWidthBehavior.ComboBoxAutoWidth="True" />
You could also just set Width directly instead of MaxWidth, and then remove the DropDownOpened and Closed parts if you want it to behave like the other anwsers.
An alternative solution to the top answer is to Measure the Popup itself rather than measuring all the items. Giving slightly simpler SetWidthFromItems()
implementation:
private static void SetWidthFromItems(this ComboBox comboBox)
{
if (comboBox.Template.FindName("PART_Popup", comboBox) is Popup popup
&& popup.Child is FrameworkElement popupContent)
{
popupContent.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
// suggested in comments, original answer has a static value 19.0
var emptySize = SystemParameters.VerticalScrollBarWidth + comboBox.Padding.Left + comboBox.Padding.Right;
comboBox.Width = emptySize + popupContent.DesiredSize.Width;
}
}
works on disabled ComboBox
es as well.
Based on the other answers above, here's my version:
<Grid HorizontalAlignment="Left">
<ItemsControl ItemsSource="{Binding EnumValues}" Height="0" Margin="15,0"/>
<ComboBox ItemsSource="{Binding EnumValues}" />
</Grid>
HorizontalAlignment="Left" stops the controls using the full width of the containing control.
Height="0" hides the items control.
Margin="15,0" allows for additional chrome around combo-box items (not chrome agnostic I'm afraid).