How can I make a WPF combo box have the width of its widest element in XAML?
You can't do it directly in Xaml but you can use this Attached Behavior. (The Width will be visible in the Designer)
<ComboBox behaviors:ComboBoxWidthFromItemsBehavior.ComboBoxWidthFromItems="True">
<ComboBoxItem Content="Short"/>
<ComboBoxItem Content="Medium Long"/>
<ComboBoxItem Content="Min"/>
</ComboBox>
The Attached Behavior ComboBoxWidthFromItemsProperty
public static class ComboBoxWidthFromItemsBehavior
{
public static readonly DependencyProperty ComboBoxWidthFromItemsProperty =
DependencyProperty.RegisterAttached
(
"ComboBoxWidthFromItems",
typeof(bool),
typeof(ComboBoxWidthFromItemsBehavior),
new UIPropertyMetadata(false, OnComboBoxWidthFromItemsPropertyChanged)
);
public static bool GetComboBoxWidthFromItems(DependencyObject obj)
{
return (bool)obj.GetValue(ComboBoxWidthFromItemsProperty);
}
public static void SetComboBoxWidthFromItems(DependencyObject obj, bool value)
{
obj.SetValue(ComboBoxWidthFromItemsProperty, value);
}
private static void OnComboBoxWidthFromItemsPropertyChanged(DependencyObject dpo,
DependencyPropertyChangedEventArgs e)
{
ComboBox comboBox = dpo as ComboBox;
if (comboBox != null)
{
if ((bool)e.NewValue == true)
{
comboBox.Loaded += OnComboBoxLoaded;
}
else
{
comboBox.Loaded -= OnComboBoxLoaded;
}
}
}
private static void OnComboBoxLoaded(object sender, RoutedEventArgs e)
{
ComboBox comboBox = sender as ComboBox;
Action action = () => { comboBox.SetWidthFromItems(); };
comboBox.Dispatcher.BeginInvoke(action, DispatcherPriority.ContextIdle);
}
}
What it does is that it calls an extension method for ComboBox called SetWidthFromItems which (invisibly) expands and collapses itself and then calculates the Width based on the generated ComboBoxItems. (IExpandCollapseProvider requires a reference to UIAutomationProvider.dll)
Then extension method SetWidthFromItems
public static class ComboBoxExtensionMethods
{
public static void SetWidthFromItems(this ComboBox comboBox)
{
double comboBoxWidth = 19;// comboBox.DesiredSize.Width;
// Create the peer and provider to expand the comboBox in code behind.
ComboBoxAutomationPeer peer = new ComboBoxAutomationPeer(comboBox);
IExpandCollapseProvider provider = (IExpandCollapseProvider)peer.GetPattern(PatternInterface.ExpandCollapse);
EventHandler eventHandler = null;
eventHandler = new EventHandler(delegate
{
if (comboBox.IsDropDownOpen &&
comboBox.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
double width = 0;
foreach (var item in comboBox.Items)
{
ComboBoxItem comboBoxItem = comboBox.ItemContainerGenerator.ContainerFromItem(item) as ComboBoxItem;
comboBoxItem.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
if (comboBoxItem.DesiredSize.Width > width)
{
width = comboBoxItem.DesiredSize.Width;
}
}
comboBox.Width = comboBoxWidth + width;
// Remove the event handler.
comboBox.ItemContainerGenerator.StatusChanged -= eventHandler;
comboBox.DropDownOpened -= eventHandler;
provider.Collapse();
}
});
comboBox.ItemContainerGenerator.StatusChanged += eventHandler;
comboBox.DropDownOpened += eventHandler;
// Expand the comboBox to generate all its ComboBoxItem's.
provider.Expand();
}
}
This extension method also provides to ability to call
comboBox.SetWidthFromItems();
in code behind (e.g in the ComboBox.Loaded event)
This can't be in XAML without either:
- Creating a hidden control (Alan Hunford's answer)
- Changing the ControlTemplate drastically. Even in this case, a hidden version of an ItemsPresenter may need to be created.
The reason for this is that the default ComboBox ControlTemplates that I've come across (Aero, Luna, etc.) all nest the ItemsPresenter in a Popup. This means that the layout of these items is deferred until they are actually made visible.
An easy way to test this is to modify the default ControlTemplate to bind the MinWidth of the outermost container (it's a Grid for both Aero and Luna) to the ActualWidth of PART_Popup. You'll be able to have the ComboBox automatically synchronize it's width when you click the drop button, but not before.
So unless you can force a Measure operation in the layout system (which you can do by adding a second control), I don't think it can be done.
As always, I'm open to an short, elegant solution -- but in this case a code-behind or dual-control/ControlTemplate hacks are the only solutions I have seen.