6

I'm trying to take into use a SelectButton (https://gist.github.com/loraderon/580405) but I need to specify MinWidth for it. Otherwise it's width is just the width of Extender. Removing ColumnSpan or setting 1st column Auto are not doing the trick. I would really like it to always have width of most wide element in list + extender symbol.

<UserControl x:Class="loraderon.Controls.SelectButton"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:my="clr-namespace:loraderon.Controls"
         mc:Ignorable="d" 
         SizeChanged="UserControl_SizeChanged"
         d:DesignHeight="30" d:DesignWidth="100">
<Grid
    x:Name="SplitGrid"
    >
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="23" />
    </Grid.ColumnDefinitions>
    <Button
        x:Name="Button"
        Click="Button_Click"
        Grid.ColumnSpan="2"
        Padding="0"
        HorizontalContentAlignment="Left"
        >
        <ContentControl
            x:Name="ButtonContent"
            HorizontalContentAlignment="Center"
            ContentTemplate="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type my:SelectButton}}, Path=ItemTemplate}"
            />
    </Button>
    <Expander
        x:Name="Expander"
        Expanded="Expander_Expanded"
        Collapsed="Expander_Collapsed"
        Grid.Column="1"
        VerticalAlignment="Center"
        IsExpanded="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type my:SelectButton}}, Path=IsExpanded}"
        />  
    <Popup
        IsOpen="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type my:SelectButton}}, Path=IsExpanded}"
        PlacementTarget="{Binding ElementName=Button}"
        PopupAnimation="Fade"
        StaysOpen="False"
        >
        <ListBox
            x:Name="ListBox"
            SelectionMode="Single"
            SelectionChanged="ListBox_SelectionChanged"
            SelectedIndex="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type my:SelectButton}}, Path=SelectedIndex, Mode=TwoWay}"
            ItemTemplate="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type my:SelectButton}}, Path=ItemTemplate}"
            ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type my:SelectButton}}, Path=ItemsSource}"
            />
    </Popup>
</Grid>
</UserControl

EDIT: The window I placed the control had:

SizeToContent="WidthAndHeight"

which resulted both answers below not to work. Is there more robust solution that would work when placing the button in variety of controls/containers? It seems that the way the control was built is not very robust. Popup not being the part of visual tree makes it a bad choice.

char m
  • 7,840
  • 14
  • 68
  • 117
  • Can't you just set the Width on it when you use it? – kenny Jun 29 '16 at 20:40
  • thanks for ur answer, but of course not. i can't know how long the items are in different languages. plus control should size to it's content correctly if size is not specified. – char m Jun 29 '16 at 20:46
  • Maybe you can sneak in some horizontal stretch properties to the user control and grid. – kenny Jun 29 '16 at 20:49
  • what i did was but this happens too late. after extending once it's ok, but initially it's not – char m Jun 29 '16 at 20:56
  • Cant you set initially MinWidth to ListBox width like MinWidth="{Binding Width, ElementName=ListBox}" and change it if necessary with visual state manager or I am missing something? – Hasan Hasanov Jun 30 '16 at 22:39
  • @HasanHasanov: The comment above yours says: " what i did was but this happens too late. after extending once it's ok, but initially it's not " -> I'm not familiar with visual state manager. Pls Write an answer. I opened a bounty :) – char m Jul 05 '16 at 08:00
  • I must not be understanding the intent. It just looks like a ComboBox with some different styling, I'm not sure what the point is? – Chris W. Jul 11 '16 at 22:06

4 Answers4

3

The easy part is binding to the ListBox' ActualWidth

   <Grid.ColumnDefinitions>
        <ColumnDefinition Width="{Binding ElementName=ListBox, Path=ActualWidth}"/>
        <ColumnDefinition Width="23" />
    </Grid.ColumnDefinitions>

The tricky part is that since the ListBox is located in a Popup, with it's own visual tree (Remarks), it only gets rendered when IsOpen is set to true.

The workaround is a swift open / close when the Control is loaded

public SelectButton()
{
    InitializeComponent();
    Loaded += (o, e) => Initialize();
}

void Initialize()
{
    IsExpanded = true;
    IsExpanded = false;
}

and an updated Expander_Expanded Method

private DateTime startUpTime = DateTime.Now;
private DateTime collapsedAt = DateTime.MinValue;

private void Expander_Expanded(object sender, RoutedEventArgs e)
{
    if (DateTime.Now - startUpTime <= TimeSpan.FromMilliseconds(200))
    {
        IsExpanded = true;
        return;
    }
    if (DateTime.Now - collapsedAt <= TimeSpan.FromMilliseconds(200))
    {
        Expander.IsExpanded = false;
        IsExpanded = false;
        return;
    }
    IsExpanded = true;
}

EDIT

Turns out the TimeSpan of 200ms can be too small dependent on the system used, added a more robust solution

private bool startUp = true;
private DateTime collapsedAt = DateTime.MinValue;

private void Expander_Expanded(object sender, RoutedEventArgs e)
{
    if (startUp)
    {
        IsExpanded = true;
        startUp = false;
        return;
    }
    if (DateTime.Now - collapsedAt <= TimeSpan.FromMilliseconds(200))
    {
        Expander.IsExpanded = false;
        IsExpanded = false;
        return;
    }
    IsExpanded = true;
}
Funk
  • 10,976
  • 1
  • 17
  • 33
  • thanks! i look into this in few hours. does opening involve visible flashing in GUI? – char m Jul 06 '16 at 11:41
  • 1
    @matti Normally no. But since you asked it did occure once, when my computer was running out of memory (getting slowisch), that just after re-compiling I could see it happen. – Funk Jul 06 '16 at 12:02
  • does not work. Expander_Expanded is called only when manually expanding. – char m Jul 06 '16 at 13:35
  • @matti After IsExpanded is set to true in SelectButton.xaml.cs (when SelectButton is loaded), Expander_Expanded is called from SelectButton.xaml (in the Expander). To duplicate my working code, you can start a new WPF Application called WpfSelectButton and implement the 4 files I posted on [Github](https://gist.github.com/FuNkNaN/7a9a7f583de2fce5c8a1c6dd651c4e50). – Funk Jul 06 '16 at 14:40
  • Thanks! I'm currently looking at the other solution which worked when I removed SizeToContent="WidthAndHeight" from my window. Could this be reason here also why the Expander_Expanded was not called? I try your edited solution later. – char m Jul 06 '16 at 19:12
  • yes! also your solution works when i remove SizeToContent="WidthAndHeight". your edit however played naturally no role in this because the method changed was not called at all. – char m Jul 06 '16 at 19:23
2

This is not pretty, but working. Since you already do Code-Behind, this might fit your needs:

First, the ItemsSourceProperty. Change it to:

 public static readonly DependencyProperty ItemsSourceProperty =
  DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(SelectButton), new PropertyMetadata(ItemsSourceChanged ));

Second, prepare Constructor:

public SelectButton() {
      InitializeComponent();
      this.ListBox.Loaded += this.ListBoxOnLoaded;
    }

Third, implement ItemnsSourceChanged-Method:

private static void ItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
      var self = d as SelectButton;
      self.ListBoxOnLoaded(self.ListBox, new RoutedEventArgs());
    }

Fourth, do the magic:

private void ListBoxOnLoaded(object sender, RoutedEventArgs routedEventArgs) {
      var lb = sender as ListBox;
      lb.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
      this.col1.MinWidth = lb.DesiredSize.Width;

    }

Last but not least, edit XAML:

<Grid x:Name="SplitGrid">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" Name="col1"  />
            <ColumnDefinition Width="23" />
        </Grid.ColumnDefinitions>

When the listbox has loaded, we simply do the measuring by ourself and apply the desired size to the first column.

Hope it helps :)

lokusking
  • 7,396
  • 13
  • 38
  • 57
  • does not work. ListBoxOnLoaded results MinWidth to be 4.0 – char m Jul 06 '16 at 13:18
  • I know. This happens on the first run, before the ItemsSource is bound. After implementing and calling the `ItemsSourceChanged`-Method, the measuring will work ;) – lokusking Jul 06 '16 at 13:35
  • same both times. i did exacctly like in ur answer. 2nd time it is called from ItemsSourceChanged. – char m Jul 06 '16 at 13:39
  • 1
    Are you sure, you modified the xaml also? Can you confirm, your ItemsSource contains items? Uploading a working .sln in about an hour – lokusking Jul 06 '16 at 13:45
  • yes. i was just gonna write to you that i know why it does not work. there's 0 items in listbox. 2 in ItemsSource. i try to check why. – char m Jul 06 '16 at 13:50
  • list box has no items before the extender is extended. itemssource on the other hand has items immediatelly after it's initialized. – char m Jul 06 '16 at 14:06
  • if the listbox could be forced to fetch the items from itemssource without extender being manually extended, then this would work. – char m Jul 06 '16 at 14:08
  • adding lb.Items.Refresh(); to ListBoxOnLoaded does nothing. only manually extending the extender results the ListBox to have items. – char m Jul 06 '16 at 14:18
  • [Here](https://1drv.ms/f/s!Asu7HJHc9puZfsCbgOFK2cSwwAg) you find a working example – lokusking Jul 06 '16 at 14:48
  • thanks! works perfect in your solution, but not mine. listbox not populated before manually extending. i just have to investigate further. – char m Jul 06 '16 at 15:26
  • You might add some code of your usage. Maybe i can find something – lokusking Jul 06 '16 at 15:28
  • my window has: Height="Auto" Width="Auto" SizeToContent="WidthAndHeight" ResizeMode="NoResize" the SizeToContent results this – char m Jul 06 '16 at 18:59
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/116636/discussion-between-lokusking-and-matti). – lokusking Jul 07 '16 at 05:26
1

This is horrible answer but might give somebody an idea. I create an invisible Listbox to same location where the button content is and bind Grid.Column="0" MinWidth to it's ActualWidth.

Somehow this is a bit too wide. The width of the ListBox is too wide to assign to Grid.Column="0". The items in the popuplistbox are a lot more narrow. Max of these should be the width assigned to Grid.Column="0".

I also tried to have a buttton there and created additional dependencyproperty for its content. That was best looking (size was perfect) but then you would have to know preferably all the items and their sizes in different languages or at least one item. This is of course huge disadvantage.

EDIT: If this same could be achieved with ContentControl/ContentPresenter somehow to avoid 2 ListBox this would be far better.

EDIT2: This does not work. The Width is width of the 1st element so order or ItemsSource is relevant.

Here is the xaml:

<UserControl x:Class="loraderon.Controls.SelectButton"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:my="clr-namespace:loraderon.Controls"
         mc:Ignorable="d"
         SizeChanged="UserControl_SizeChanged"
         d:DesignHeight="30" d:DesignWidth="100">
  <Grid
      x:Name="SplitGrid"
    >
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" MinWidth="{Binding ActualWidth, ElementName=ContentListBox}"/>
      <ColumnDefinition Width="23" />
    </Grid.ColumnDefinitions>
    <Button
        x:Name="Button"
        Click="Button_Click"
        Grid.ColumnSpan="2"
        Padding="0"
        HorizontalContentAlignment="Left"
        >
      <ContentControl
          x:Name="ButtonContent"
          HorizontalContentAlignment="Center"
          ContentTemplate="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type my:SelectButton}}, Path=ItemTemplate}"
            />
    </Button>
    <ListBox
        Grid.Column="0"
            x:Name="ContentListBox"
            Visibility="Hidden"
            MaxHeight="{Binding ActualHeight, ElementName=Button}"
            HorizontalAlignment="Stretch"
            ItemTemplate="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type my:SelectButton}}, Path=ItemTemplate}"
            ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type my:SelectButton}}, Path=ItemsSource}"/>
    <Expander
        x:Name="Expander"
        Expanded="Expander_Expanded"
        Collapsed="Expander_Collapsed"
        Grid.Column="1"
        VerticalAlignment="Center"
        IsExpanded="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type my:SelectButton}}, Path=IsExpanded}"
        />
    <Popup
        IsOpen="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type my:SelectButton}}, Path=IsExpanded}"
        PlacementTarget="{Binding ElementName=Button}"
        PopupAnimation="Fade"
        StaysOpen="False"
        >
      <ListBox
          x:Name="ListBox"
          SelectionMode="Single"
          SelectionChanged="ListBox_SelectionChanged"
          SelectedIndex="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type my:SelectButton}}, Path=SelectedIndex, Mode=TwoWay}"
          ItemTemplate="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type my:SelectButton}}, Path=ItemTemplate}"
          ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type my:SelectButton}}, Path=ItemsSource}"
            />
    </Popup>
  </Grid>
</UserControl
char m
  • 7,840
  • 14
  • 68
  • 117
  • if ListBox could be defined only once, this would be good. Could that be done as a resource? I tried, but couldn't make it happen. Any answer which makes this solution better (e.g. avoiding 2 ListBox:es) will be accepted. – char m Jul 07 '16 at 10:02
1

You can create a temporary ListBox and measure it to find the desired size for the element.

The most appropriate place to compute the size is when the ItemsSource property changes. You can achieve this by modifying the dependency property as such:

public static readonly DependencyProperty ItemsSourceProperty =
        DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(SelectButton), new PropertyMetadata(ItemSourceChanged));

In the ItemSourceChanged method you can create a temporary ListBox, make it have your items, and measure it:

private static void ItemSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    ContentControl control = new ContentControl();
    ListBox listBox = new ListBox();
    control.Content = listBox;

    IEnumerable enumerable = e.NewValue as IEnumerable;
    SelectButton selectButton = d as SelectButton;

    foreach (var item in enumerable)
    {
        listBox.Items.Add(item);
    }

    listBox.Measure(new Size(Double.MaxValue, Double.MaxValue));
    selectButton.Button.Width = listBox.DesiredSize.Width;
}

Here the line control.Content = listBox; is necessary. If the ListBox is not contained within a control, desired size always returns 0.

Yusuf Tarık Günaydın
  • 3,016
  • 2
  • 27
  • 41
  • does not work. if 2nd item is like "Item very very very long" the control is not long enough. I tried also setting width if ButtonContent instead of Button and removing UserControl_SizeChanged – char m Jul 12 '16 at 17:23
  • Are you sure? I can successfully create a button with length of the longest item. I can send you a working example. How do you use the control in your window? – Yusuf Tarık Günaydın Jul 12 '16 at 19:18