I know how to do it in code, but can this be done in XAML ?
Window1.xaml:
I ended up with a "good enough" solution to this problem being to make the combo box never shrink below the largest size it held, similar to the old WinForms AutoSizeMode=GrowOnly.
The way I did this was with a custom value converter:
public class GrowConverter : IValueConverter
{
public double Minimum
{
get;
set;
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var dvalue = (double)value;
if (dvalue > Minimum)
Minimum = dvalue;
else if (dvalue < Minimum)
dvalue = Minimum;
return dvalue;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
Then I configure the combo box in XAML like so:
<Whatever>
<Whatever.Resources>
<my:GrowConverter x:Key="grow" />
</Whatever.Resources>
...
<ComboBox MinWidth="{Binding ActualWidth,RelativeSource={RelativeSource Self},Converter={StaticResource grow}}" />
</Whatever>
Note that with this you need a separate instance of the GrowConverter for each combo box, unless of course you want a set of them to size together, similar to the Grid's SharedSizeScope feature.
In my case a much simpler way seemed to do the trick, I just used an extra stackPanel to wrap the combobox.
<StackPanel Grid.Row="1" Orientation="Horizontal">
<ComboBox ItemsSource="{Binding ExecutionTimesModeList}" Width="Auto"
SelectedValuePath="Item" DisplayMemberPath="FriendlyName"
SelectedValue="{Binding Model.SelectedExecutionTimesMode}" />
</StackPanel>
(worked in visual studio 2008)
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)
A follow up to Maleak's answer: I liked that implementation so much, I wrote an actual Behavior for it. Obviously you'll need the Blend SDK so you can reference System.Windows.Interactivity.
XAML:
<ComboBox ItemsSource="{Binding ListOfStuff}">
<i:Interaction.Behaviors>
<local:ComboBoxWidthBehavior />
</i:Interaction.Behaviors>
</ComboBox>
Code:
using System;
using System.Windows;
using System.Windows.Automation.Peers;
using System.Windows.Automation.Provider;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Interactivity;
namespace MyLibrary
{
public class ComboBoxWidthBehavior : Behavior<ComboBox>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += OnLoaded;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.Loaded -= OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
var desiredWidth = AssociatedObject.DesiredSize.Width;
// Create the peer and provider to expand the comboBox in code behind.
var peer = new ComboBoxAutomationPeer(AssociatedObject);
var provider = peer.GetPattern(PatternInterface.ExpandCollapse) as IExpandCollapseProvider;
if (provider == null)
return;
EventHandler[] handler = {null}; // array usage prevents access to modified closure
handler[0] = new EventHandler(delegate
{
if (!AssociatedObject.IsDropDownOpen || AssociatedObject.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
return;
double largestWidth = 0;
foreach (var item in AssociatedObject.Items)
{
var comboBoxItem = AssociatedObject.ItemContainerGenerator.ContainerFromItem(item) as ComboBoxItem;
if (comboBoxItem == null)
continue;
comboBoxItem.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
if (comboBoxItem.DesiredSize.Width > largestWidth)
largestWidth = comboBoxItem.DesiredSize.Width;
}
AssociatedObject.Width = desiredWidth + largestWidth;
// Remove the event handler.
AssociatedObject.ItemContainerGenerator.StatusChanged -= handler[0];
AssociatedObject.DropDownOpened -= handler[0];
provider.Collapse();
});
AssociatedObject.ItemContainerGenerator.StatusChanged += handler[0];
AssociatedObject.DropDownOpened += handler[0];
// Expand the comboBox to generate all its ComboBoxItem's.
provider.Expand();
}
}
}
Yeah, this one is a bit nasty.
What I've done in the past is to add into the ControlTemplate a hidden listbox (with its itemscontainerpanel set to a grid) showing every item at the same time but with their visibility set to hidden.
I'd be pleased to hear of any better ideas that don't rely on horrible code-behind or your view having to understand that it needs to use a different control to provide the width to support the visuals (yuck!).
I was looking for the answer myself, when I came across the UpdateLayout()
method that every UIElement
has.
It's very simple now, thankfully!
Just call ComboBox1.Updatelayout();
after you set or modify the ItemSource
.