Expander, Grid & ListBox = No Virtualization - wpf

I have a ListBox, inside a Grid, inside an Expander. The ListBox is bound to an IList.
When I expand the Expander control for the first time, the ListBox processes all of the items in the IList (which can be thousands) instead of only processing the items that would be visible on the screen.
If however I fix the height of the ListBox control, it behaves as expected and only accesses those items in the IList that will be visible.
Effectively, the Virtualization is not working, though I believe that this is more related to the ListBox not being able to determine a height when the content items are being prepared.
The XAML is basically as follows (some stuff removed for simplification)...
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Expander ExpandDirection="Right"
Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="2"
Header="Documents"
IsExpanded="False">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition Height="50" />
</Grid.RowDefinitions>
</Grid>
<ListBox Name="listBox"
Grid.Row="1"
ItemsSource="{Binding Path=Items}"
SelectedIndex="{Binding Path=SelectedIndex}"
SelectedItem="{Binding Path=SelectedItem}"
SelectionMode="Single"
Width="250">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Column="0"
Style="{StaticResource prompt}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:00000}">
<Binding Path="..."
FallbackValue="0" />
<Binding Path="..." />
</MultiBinding>
</TextBlock.Text></TextBlock>
<TextBlock Grid.Column="1"
Style="{StaticResource prompt}">
<TextBlock.Text>
<Binding Path="ItemCount"
StringFormat="{}{0} Items"
FallbackValue="" />
</TextBlock.Text></TextBlock>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Expander>
<v:DocumentView x:Name="documentView"
Grid.Column="1"
Grid.Row="0"
DocumentID="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type v:BatchView}}, Path=ViewModel.SelectedItem.ID}"
IsActive="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type v:BatchView}}, Path=IsActive}" />
<StackPanel Grid.Column="1"
Grid.Row="1"
Style="{StaticResource buttonStackStyle}">
<Button Command="{Binding Path=PreviousCommand}"
Style="{StaticResource previousButtonStyle}" />
<Button Command="{Binding Path=NextCommand}"
Style="{StaticResource nextButtonStyle}" />
</StackPanel>
</Grid>
Can anybody suggest how I might set the Height of the ListBox to the ActualHeight of the Grid.Row parent? Alternatively, can anybody provide a better solution?
Thanks.

Short version: remove Grid.RowSpan from the expander.
Long version:
Background: (in really broad strokes)
When you define the height of RowDefinition three things can happen, depending what type of units you end up using:
Pixel - any UIElement that is placed in that row will have the defined row height passed to the Measure and Arrange methods of the element.
Auto - the grid will pass infinity as the height for Measure and then element.DesiredSize.Height for Arrange.
Star - the grid will consider the heights of all rows with units of pixels and auto; calculate the height that is left from its available height and divide it by the "total of stars" that were defined for all the rows - this is the height of one star; then, each row height is assigned depending on the multiplier for its star definition; this height is passed to the Measure and Arrange methods.
Same logic is applied to column definitions only regarding width instead of height.
So, star definition is "stopping" the element, pixel definition is also "stopping" but it can be outside of the rendered view and auto definition is "letting" the element to be what ever size it wants to be.
All this logic is recursive so you need to think in two directions (explanation below).
In your case
In one direction. The ListBox is in a star row so it'll be stopped. The parent grid is also stopped (since the template for expander uses DockPanel that is also a "stopping panel"). The expander is defined to begin in a star row but it spans to an auto row - this means that it will be allowed to grow in height 'till infinity. Oops...time to reverse.
Now the reverse direction. The expander is not stopped, the child grid is not stopped (since the grid assumes it has infinite height available), thus the list box is not stopped, the ScrollViewer in the template of list box is not stopped so it's ViewportHeight is infinite, for the VirtualizingStackPanel that arranges the items (and is a child of the scroll viewer) this means all items are in the view == render all elements.
For a WPF window with default template, you can always assume the window is stopping its child element. So if removing the row span definition has not resolved the issue, continue traversing up until you find another element that is not stopping its child height and change its definitions or change the panel to stop the height from growing to infinity (scroll viewers are notorious for creating these behaviours, especially the ones that are hidden in templates).

Related

How to make scrollviewer work with Height set to Auto in WPF?

I have learned that if the height of a grid row, where the ScrollViewer resides, is set as Auto, the vertical scroll bar will not take effect since the actual size of the ScrollViewer can be larger than the height in sight. So in order to make the scroll bar work, I should set the height to either a fixed number or star height
However, I now have this requirement, that I have two different views reside in two grid rows, and I have a toggle button to switch between these two views: when one view is shown, the other one is hidden/disappeared. So I have defined two rows, both heights are set as Auto. And I bind the visibility of the view in each row to a boolean property from my ViewModel (one is converted from True to Visible and the other from True to Collapsed. The idea is when one view's visibility is Collapsed, the height of the grid row/view will be changed to 0 automatically.
The view show/hidden is working fine. However, in one view I have a ScrollViewer, which as I mentioned doesn't work when the row height is set as Auto. Can anybody tell me how I can fulfill such requirement while still having the ScrollViewer working automatically`? I guess I can set the height in code-behind. But since I am using MVVM, it would require extra communication/notification. Is there a more straightforward way to do that?
In MVVM, the way that worked for me was to bind the height of the ScrollViewer to the ActualHeight of the parent control (which is always of type UIElement).
ActualHeight is a read-only property which is only set after the control has been drawn onto the screen. It may change if the window is resized.
<StackPanel>
<ScrollViewer Height="{Binding Path=ActualHeight,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UIElement}}">
<TextBlock Text=Hello"/>
</ScrollViewer>
</StackPanel>
But what if the parent control has an infinite height?
If the parent control has an infinite height, then we have a bigger problem. We have to keep setting the height of all parents, until we hit a control with a non-infinite height.
Snoop is absolutely invaluable for this:
If the "Height" for any XAML element is 0 or NaN, you can set it to something using one of:
Height="{Binding Path=ActualHeight, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UIElement}}"
VerticalAlignment="Stretch"
Height="Auto"
Hint: Use VerticalAlignment="Stretch" if you are a child of a Grid with a <RowDefinition Height="*">, and the Binding RelativeSource... elsewhere if that doesn't work.
If you're interested, here is all of my previous attempts to fix this issue:
Appendix A: Previous Attempt 1
Can also use this:
Height="{Binding Path=ActualHeight, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=StackPanel}}"
Appendix B: Previous Attempt 2
Useful info: see Auto Height in combination with MaxHeight.
If nothing seems to work, it's probably because the ActualHeight of the parent is either 0 (so nothing is visible) or huge (so the scrollviewer never needs to appear). This is more of a problem if there are deeply nested grids, with a scrollviewer right at the bottom.
Use Snoop to find the ActualHeight of the parent StackPanel. In properties, filter by the word "Actual", which brings back ActualHeight and ActualWidth.
If ActualHeight is zero, give it a minimum height using MinHeight, so we can at least see something.
If ActualHeight is so huge that it goes off the edge of the screen (i.e. 16,000), give it a reasonable maximum height using MaxHeight, so the scrollbars will appear.
Once the scrollbars are appearing, then we can clean it up further:
Bind the Height of the StackPanel or Grid to the ActualHeight of the parent.
Finally, put a ScrollViewer inside this StackPanel.
Appendix C: Previous Attempt 3
It turns out that this can sometimes fail:
Height="{Binding Path=ActualHeight, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=StackPanel}}"
The reason? It the binding fails, the height will be zero and nothing will be seen. The binding can fail if we are binding to an element which is not accessible. The binding will fail if we are going up the visual tree, then down to a leaf node (e.g. up to the parent grid, then down to the ActualHeight of a row attached to that grid). This is why binding to the ActualWidth of a RowDefinition simply won't work.
Appendix D: Previous Attempt 4
I ended up getting this working by making sure that Height=Auto for all of the parent elements from us to the first <Grid> element in the UserControl.
Change Height from Auto to *, if you can.
Example:
<Window x:Class="WpfApplication3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="200" Width="525">
<StackPanel Orientation="Horizontal" Background="LightGray">
<Grid Width="100">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ScrollViewer VerticalScrollBarVisibility="Auto" x:Name="_scroll1">
<Border Height="300" Background="Red" />
</ScrollViewer>
<TextBlock Text="{Binding ElementName=_scroll1, Path=ActualHeight}" Grid.Row="1"/>
</Grid>
<Grid Width="100">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ScrollViewer VerticalScrollBarVisibility="Auto" x:Name="_scroll2">
<Border Height="300" Background="Green" />
</ScrollViewer>
<TextBlock Text="{Binding ElementName=_scroll2, Path=ActualHeight}" Grid.Row="1"/>
</Grid>
</StackPanel>
</Window>
I've had similar problem, taking me hours to figure out the solution. What solved it was using a Dockpanel as parent container instead of a StackPanel. Just specify all children to dock to top if the functionality should be similar to vertical stackpanel. Consider using LastChildFill="False" in the Dock XAML which is'n default.
So instead of:
<StackPanel Orientation="Horizontal">
<Textbox>SomeTextBox</Textbox>
<Scrollviewer/>
</StackPanel>
Try:
<DockPanel LastChildFill="False">
<Textbox DockPanel.Dock="Top">SomeTextBox</Textbox>
<Scrollviewer DockPanel.Dock="Top"/>
</DockPanel>
You can either set a fix height on your ScrollViewer but then you have to consider that the second row of your grid will have that height too since row's first child will be the ScrollViewer and row's height is auto, or you bind the height of ScrollViewer to another control in your layout. We don't know how your layout looks alike.
At the end if you don't like neither of both just set the row's height to * as swiszcz suggested or hack wpf write your own custom panel that will be able to layout everything possible in every parallel universe or something like that. :)
What I discover is that you have to put your ScrollViewer within a container that has Height=Auto or you get his parent Heigh Actual Size and apply it to that container.
In my case I have UserControl like
<Grid Margin="0,0,0,0" Padding="0,2,0,0">
<ScrollViewer Height="Auto" ZoomMode="Disabled" IsVerticalScrollChainingEnabled="True" VerticalAlignment="Top"
HorizontalScrollMode="Enabled" HorizontalScrollBarVisibility="Disabled"
VerticalScrollMode="Enabled" VerticalScrollBarVisibility="Visible">
<ListView ItemsSource="{x:Bind PersonalDB.View, Mode=OneWay}" x:Name="DeviceList"
ScrollViewer.VerticalScrollBarVisibility="Hidden"
ItemTemplate="{StaticResource ContactListViewTemplate}"
SelectionMode="Single"
ShowsScrollingPlaceholders="False"
Grid.Row="1"
Grid.ColumnSpan="2"
VerticalAlignment="Stretch"
BorderThickness="0,0,0,0"
BorderBrush="DimGray">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel AreStickyGroupHeadersEnabled="False" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate x:DataType="local1:GroupInfoList">
<TextBlock Text="{x:Bind Key}"
Style="{ThemeResource TitleTextBlockStyle}"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
</ScrollViewer>
</Grid>
And I add it dinamically to ContentControl which is within a Page.
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Margin="0,0,12,0">
<Grid.RowDefinitions>
<RowDefinition Height="70" />
<RowDefinition Height="*" MinHeight="200" />
</Grid.RowDefinitions>
<Grid Grid.Row="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" >
<ContentControl x:Name="UIControlContainer" />
</Grid>
</Grid>
Notice that Heigh of the Row is *
When I populate ContentControl I use this code in Loaded event
UIControlContainer.Content = new UIDeviceSelection() {
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Stretch,
Height = UIControlContainer.ActualHeight,
Width = UIControlContainer.ActualWidth
};
And also when ContentControl changes its size you have to update size of the UserControl.
UIControlContainer.SizeChanged += UIControlContainer_SizeChanged;
private void UIControlContainer_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (UIControlContainer.Content != null)
{
if (UIControlContainer.Content is UserControl)
{
(UIControlContainer.Content as UserControl).Height = UIControlContainer.ActualHeight;
(UIControlContainer.Content as UserControl).Width = UIControlContainer.ActualWidth;
}
}
}
Enjoy!
P.S. Acctually I did it for UWP.

Listbox content not dynamically re-sizing when Listbox size changes

I've seen quite a few people asking this question and i feel like my question is slightly different. I have a listbox that holds a series of identical custom made user controls. These controls are added to the listbox at runtime. Now i currently have my listbox items resizing themselves properly upon first creation and insertion into the control.
Here is the strange part. If I resize the listbox the controls that have been visible previously are not resized to the new listbox width. In other words if i add 12 controls and the box only shows 4 (the rest are hidden by a scrollbar) then if i resize the box the first 4 controls will still be the original width and if i scroll then the other 8 will be the correct size. Also if i manipulate the list items in any way they resize themselves to the proper width automatically. SEE EDIT2
I've tried attaching to the sizeChanged event and issuing the following on both the listbox and the items but it has had no effect. I think i need to find some way of resetting the layout information for the listbox items but i can't find the command.
item.InvalidateArrange();
item.InvalidateMeasure();
Layers.UpdateLayout();
item.UpdateLayout();
I think this has something to do with the items i'm adding because even if i detach the items from the lisbox and then attach them they remain the wrong width.
Here is my listbox code:
<ListBox x:Name="Layers" VerticalContentAlignment="Top" Margin="0,17,0,0" BorderThickness="0,1,0,0" HorizontalContentAlignment="Stretch" SizeChanged="Layers_SizeChanged">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
Here is the code for my items
<UserControl x:Class="ListOverlayItem"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:data="clr-namespace:Data"
HorizontalContentAlignment="Stretch">
<UserControl.Resources>
<data:Translator x:Key="translatorString" />
</UserControl.Resources>
<Border BorderBrush="Silver" BorderThickness="0,0,0,0" Name="border1" HorizontalAlignment="Stretch">
<Grid x:Name="layout" HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="19"/>
<RowDefinition Height="25" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="60" />
</Grid.ColumnDefinitions>
<!-- Column 0 -->
<ScrollBar Name="arrangeIcon" FlowDirection="LeftToRight" Maximum="10" SmallChange="1" Value="5" Grid.Column="0" Grid.Row="0" Grid.RowSpan="2"/>
<!-- Column 1 -->
<Slider Name="OverlayOpacity" Height="25" Grid.Column="1" Margin="6,0,0,0" Grid.Row="1" Maximum="1" LargeChange="0.1" ValueChanged="OverlayOpacity_ValueChanged"/>
<TextBlock x:Name="OverlayName" Text="{Binding Path=LocationName}" Foreground="#FFF08D2D" Margin="10,2,0,0" Grid.Column="1" FontSize="12" Height="18" VerticalAlignment="Top" />
<!-- Column 3 -->
<Button Name="SettingsButton" Grid.Column="3" Content="{Binding TRK_OV_Settings, Source={StaticResource translatorString}}" VerticalAlignment="Center" Click="SettingsButton_Click" />
<CheckBox x:Name="OverlayEnabled" FlowDirection="LeftToRight" Grid.Column="2" DataContext="{Binding}" HorizontalAlignment="Right" VerticalAlignment="Center" Grid.RowSpan="2" Checked="OverlayEnabled_Checked" Unchecked="OverlayEnabled_Unchecked" />
<TextBlock Name="percentage" Text="100%" FontSize="9" TextAlignment="Right" Grid.Column="2" Grid.Row="1" VerticalAlignment="Center" Margin="2,6,26,6" MinWidth="30"/>
</Grid>
</Border>
Again it seems like my UserControl is correctly able to scale itself its just not getting the command to do so when the parent container is resized.
EDIT: Whoops, removed the WPF tag because it was incorrectly added. I think i've got it down to slider being the culprit. If i set the slider to a fixed size instead of 'stretch' then the item correctly scales just fine. So the question now is how do i force the slider to resize itself.
EDIT2: I know what is causing the issue but i don't know how to solve it. What is happening is the slider that i have in my user control will NOT resize along with the rest of the control unless i change the value of the slider during the resize. The instant i change its value even a fraction it resizes itself automatically. How can i force it to resize?
I determined that the slider was causing the issue and i tried many ways to force the slider to re-draw when it needed to but i was unsuccessful. My final solution was to remove the slider entirely. Works fine now.

How to bind Font Size to variable Grid Size

I have a grid with 2 columns and 2 rows. A single character (Unicode U+2699) was placed inside the bottom right grid field. It looks like this:
I´d like the character to automatically adjust its font size to fit the grid field it has been placed in (in this case it should be bound to the height of the second grid row, but since in some cases it could be unclear if the height or the width of the grid is smaller, it would be also nice to know how to bind to the lowest of those 2 values).
My implementation so far is something like the following (I simplified it a bit for this example):
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition x:Name="heightToBind" Height="40"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="14*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button FontSize="{Binding ElementName=heightToBind, Path=Height.Value, Mode=OneWay}" Content="⚙" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right" />
</Grid>
The problem here is, that it only works if the height is a fixed value inside the RowDefinitions. I want it to work with the following definition:
<Grid.RowDefinitions>
<RowDefinition Height="4*"/>
<RowDefinition x:Name="heightToBind" Height="*"/>
</Grid.RowDefinitions>
As a bonus question I´d also be interested why it might be that the character is placed too low so it is getting cut off at the bottom (I tried VerticalAlignment="Center" for the button but with no effect).
You can try using a ViewBox as the button's content:
<Button Grid.Row="1" Grid.Column="1">
<Button.Content>
<Viewbox StretchDirection="Both" HorizontalAlignment="Stretch">
<TextBlock Text="⚙" />
</Viewbox>
</Button.Content>
</Button>
A ViewBox can stretch and scale his child to fill all the available space...
You could try binding to the ActualHeight instead of the Height:
<Button FontSize="{Binding ElementName=heightToBind, Path=ActualHeight.Value, Mode=OneWay}"
Content="⚙" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right" />
This should work.
The * on the grid definition means take the available space as the height so it's only determined when the page layout has been prepared for layout. If the height is either unset or changed then the real height is returned in the ActualHeight property.

WPF TextBox Wrapping

I am trying to figure out how to get a textbox to wrap its contents, however the situation isn't quite the same as the typical "it doesn't wrap" scenario. My textbox is contained inside a DataTemplate which is used inside a Telerik RadTabControl instance (using a ContentTemplatePresenter to determine which view to display) and the XAML for the DataTemplate looks like this:
<DataTemplate x:Key="NotesTemplate">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Text="Use the box below to record any general notes associated with this item." Style="{StaticResource Default}" />
<TextBox TextWrapping="Wrap" AcceptsReturn="True" VerticalScrollBarVisibility="Auto" GridRow="1" Margin="20" Text="{Binding Notes, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</DataTemplate>
The reason I say it doesn't fall under the normal "it doesn't wrap" scenario is it used to wrap until I had to change the view to be resizable to anything to support the varying screen sizes the app will be run on. When I did that the TextBox stopped wrapping because (presumably) as the user types something the TextBox says "I need more space" so the parent obliges and the box continues out to the right indefinitely (although the view gets scrollbars). I tried setting a MaxWidth using Binding/RelativeSource, but since the parent is specifically designed to grow that approach won't work. What I need to have happen is the box should be the width of its' containing parents' VisibleWidth. Meaning, if the Window itself is 1024x768, the TextBox's MaxWidth should be 1024 and any text thereafter would automatically wrap, but if the Window grows to 1280x1024 the box should now be 1280 and the text wrap accordingly. I tried this scenario with this binding expression, but no luck:
MaxWidth="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=ActualWidth}"
The Window size itself isn't growing so if I could get the Window's Width (minus a certain amount to cover the width of the tabs that are part of the TabControl) I believe that would work.
Any ideas?
although the view gets scrollbars
Disable the horizontal scrollView, so it will be forced to wrap. You can try to disable it on the TextBox itself, or on the wrapping Grid.
<DataTemplate x:Key="NotesTemplate">
<Grid ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Text="Use the box below to record any general notes associated with this item." Style="{StaticResource Default}" />
<TextBox TextWrapping="Wrap" AcceptsReturn="True" VerticalScrollBarVisibility="Auto" Grid.Row="1" Margin="20" Text="{Binding Notes, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</DataTemplate>

WPF buttons same/recommended width

Suppose you have a window with multiple buttons such as Ok/Cancel or Yes/No/Cancel. All the buttons need to be the same width. Obviously this could be done by just guessing a number and hardwiring all of them to that number.
Is there a better way to do it, one that would take into account preferred/recommended sizes (just how wide should an Ok button be anyway? This is not a rhetorical question, I actually don't know the answer!), what's needed by the text of the longest caption, what happens if the font size is increased etc?
Another, perhaps simpler, way to do this is to use the SharedSizeGroup property on the ColumnDefinition and RowDefinition classes.
Columns (and Rows) in a WPF Grid can automatically resize to fit their contents - when SharedSizeGroup is used, columns with the same group name share their resizing logic.
The Xaml would look something like this ...
<Grid Grid.IsSharedSizeScope="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition SharedSizeGroup="Buttons" />
<ColumnDefinition SharedSizeGroup="Buttons" />
<ColumnDefinition SharedSizeGroup="Buttons" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Button Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Content="Ok"
Margin="4" />
<Button Grid.Column="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Content="Cancel"
Margin="4" />
<Button Grid.Column="3"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Content="Long Button Caption"
Margin="4" />
</Grid>
There are several ways to do this:
1) Use a Grid for layout. Each Button gets its own Column, which is Star-sized. That way, all columns are the same size:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0">Yes</Button>
<Button Grid.Column="1">No</Button>
<Button Grid.Column="2">Cancel</Button>
</Grid>
2) You can have one item as "master size" and bind the width of all others to this item's width.
<StackPanel Orientation="Horizontal">
<Button Name="MasterButton" Width="100">Yes</Button>
<Button>
<Button.Width>
<Binding ElementName="MasterButton" Path="Width"/>
</Button.Width>
No
</Button>
</StackPanel>
EDIT: In actual code, you probably will have Width="Auto". Since the other widths are based on the "master width", the button with the widest width (widest text) should be chosen.
Use a "master" control, like in Daniel's answer, but bind to the "ActualWidth" attribute instead of "Width":
<StackPanel Orientation="Horizontal">
<Button Name="MasterButton">Yes</Button>
<Button>
<Button.Width>
<Binding ElementName="MasterButton" Path="ActualWidth"/>
</Button.Width>
No
</Button>
</StackPanel>
This way, the value is taken from the master control at run time, after minimum and maximum width and all other layout calculations have been taken into account. Binding to "Width" binds to whatever you happen to put into the attribute at compile time, which may not be the width that is really used.
Also, the binding can be written shorter like
<Button Width="{Binding ElementName=MasterButton, Path=ActualWidth}"/>
According to the MS User Experience Interaction Guidelines for Windows 7 and Windows Vista (p61), standard dimensions for command buttons are 50x14 DLU actual size (75x23 pixels). The guidelines further suggest you "try to work with [these] default widths and heights." Obviously, if you need more width to fit a clear label, then take more width.
These answers are great if you have a fixed number or fixed layout for the buttons, but if like me there is a dynamic number of buttons coming from a binding and contained in a ItemsControl then this is not feasible. But there is a simple way and it still involves used the sharedsize property of Grid.
DataTemplate:
<DataTemplate x:Key="ODIF.Mapping">
<Button HorizontalContentAlignment="Left" Background="#FFEEEEEE" BorderBrush="#FFBDBDBD">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="PluginButtonsWidth"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" SharedSizeGroup="PluginButtonsIconHeight"/>
<RowDefinition Height="Auto" SharedSizeGroup="PluginButtonsNameHeight"/>
</Grid.RowDefinitions>
<Image Width="32" Height="32" Source="{Binding PluginIcon}" RenderOptions.BitmapScalingMode="HighQuality"/>
<TextBlock Grid.Row="1" Text="{Binding PluginName}"/>
</Grid>
</Button>
</DataTemplate>
Parent container:
<ItemsControl ItemsSource="{Binding MappingPlugins, ElementName=page}" ItemTemplate="{StaticResource ODIF.Mapping}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Grid.IsSharedSizeScope="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Essentially the button's content can itself be a Gird which then you can place your labels and icons as needed in, but even though the buttons do not reside in the same grid (they each are their own) the grid can still share it size so long as you set the root container's (ItemsControl) property of Grid.IsSharedSizeScope to True.
This will force the content grid of each button to be the same exact size based on the largest one while not having to have the Buttons themselves in a predefined grid.
In the most general case, you want to create a
Style in your section, then apply this style as desired. Now when you change the style, all buttons change.
Or you can change the Content of the button so that it autosizes to the text.

Resources