Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make a "SelectButton" with Button, Extender and ListBox to have width it needs?

Tags:

wpf

xaml

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.

like image 869
char m Avatar asked Jun 29 '16 20:06

char m


3 Answers

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;
}
like image 186
Funk Avatar answered Nov 20 '22 07:11

Funk


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 :)

like image 26
lokusking Avatar answered Nov 20 '22 08:11

lokusking


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
like image 1
char m Avatar answered Nov 20 '22 07:11

char m