When text wraps within TextBlock, ActualHeight is incorrect - wpf

I have a TextBlock with a Border around it that's inside of a Canvas that I'm using to animate it as part of a custom control. The block slides in from the bottom of the screen over the top of the image. I'm trying to use the ActualHeight of the TextBlock to determine how far to move it onto the page, but when there is so much text that it wraps to two lines, the ActualHeight returns the same size as though there was a single line.
TextBlock:
<DataTemplate DataType="{x:Type contentTypes:BusinessAdText}" x:Key="BusinessAdTextTemplate">
<Border Background="#a9a9a975"
Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type Canvas}}, Path=ActualWidth}">
<TextBlock Margin="20" Text="{Binding Text}"
TextWrapping="Wrap">
</TextBlock>
</Border>
</DataTemplate>
This style is applied which has the canvas:
<Style TargetType="local:BusinessAd">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:BusinessAd">
<Border Background="Transparent">
<Canvas ClipToBounds="True">
<ContentPresenter x:Name="PART_Content"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
</Canvas>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Code behind for BusinessAd.cs has:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_contentPart = GetTemplateChild("PART_Content") as FrameworkElement;
}
Then just using a simple DoubleAnimation I move it onto the screen:
if (_contentPart != null && _isLoaded)
{
_storyboard.Stop();
vAnimation.From = ActualHeight;
vAnimation.To = ActualHeight - _contentPart.ActualHeight;
//_contentPart.ActualHeight returns 46.something no matter how much text is there
vAnimation.Duration = new Duration(TimeSpan.FromSeconds(Duration));
if (_storyboard.Children.Count == 0)
{
_storyboard.Children.Add(vAnimation);
Storyboard.SetTargetProperty(vAnimation, new PropertyPath("(Canvas.Top)"));
Storyboard.SetTarget(vAnimation, _contentPart);
}
_storyboard.Begin();
}

You have to call UpdateLayout() before checking ActualHeight:
if (_contentPart != null && _isLoaded)
{
_storyboard.Stop();
UpdateLayout();
vAnimation.From = ActualHeight;
vAnimation.To = ActualHeight - _contentPart.ActualHeight;
//_contentPart.ActualHeight returns 46.something no matter how much text is there
vAnimation.Duration = new Duration(TimeSpan.FromSeconds(Duration));
if (_storyboard.Children.Count == 0)
{
_storyboard.Children.Add(vAnimation);
Storyboard.SetTargetProperty(vAnimation, new PropertyPath("(Canvas.Top)"));
Storyboard.SetTarget(vAnimation, _contentPart);
}
_storyboard.Begin();
}

I'm not sure if this applies to you, but for me, the textblock in Windows.UI.Xaml.Controls needs to be preceded with this:
myTextBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Before, when I had just myTextBlock.Measure(new Size());, it worked when there wasn't any text wrapping, but with wrapping ActualWidth and ActualHeight returned the dimensions of the word/letter, depending on WrapWholeWords or Wrap

Related

WPF dynamically create button by style and set control elements inside

We have a style defined as follow:
<Style x:Key="StartButtonStyle" TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Button ... Style="{StaticResource StartBtnStyle}">
<Button.Content>
<StackPanel>
<TextBlock x:Name="Line1" Text="..." FontSize="20" />
<TextBlock x:Name="Line2" Text="..." FontSize="8" />
</StackPanel>
</Button.Content>
</Button>
</ControlTemplate>
</Setter.Value>
</Setter>
We creates a button dynamically:
var button = new Button() {
Margin = new Thickness(3d,3d,3d,10d),
Style = FindResource("StartButtonStyle") as Style,
};
We want to find the "Line1" textblock inside the new button, and set the Text property:
var line1 = (TextBlock)button.FindName("Line1");
But it finds only "null" :( How should we find the textblock inside the button? Any advice is appreciated! Thanks in advance!
Wait until the Style has been applied - there is no TextBlock elment before this - to the Button and then find the TextBlock in the visual tree:
var button = new Button()
{
Margin = new Thickness(3d, 3d, 3d, 10d),
Style = FindResource("StartButtonStyle") as Style,
};
button.Loaded += (s, e) =>
{
TextBlock line1 = FindChild<TextBlock>(button, "Line1");
if(line1 != null)
{
line1.Text = "...";
}
};
The recursive FindChild<T> method is from here.

Horizontal accordion control?

I want a control whose behavior is as follows:
Act like a Grid
Each child control is embedded in a horizontal Expander (whose header is binded to the control's Tag property)
Each of these Expander has its own ColumnDefinition
Only one of these expanders can be expanded at a time
Non-expanded Expanders' ColumnDefinition have a width set to Auto
The expanded Expander's one is * (Star)
It has to use these exact controls (Grid/Expander), and not some custom ones, so my application's style can automatically apply to them.
I can't seem to find something already made, no built-in solution seems to exist (if only there was a "filling" StackPanel...) and the only solution I can come up with is to make my own Grid implementation, which seems... daunting.
Is there a solution to find or implement such a control?
Here's what I have for now. It doesn't handle the "single-expanded" nor the filling. I don't really know if StackPanel or Expander is to blame for this.
<ItemsControl>
<ItemsControl.Resources>
<DataTemplate x:Key="verticalHeader">
<ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type Expander}}, Path=Header}" />
</DataTemplate>
<Style TargetType="{x:Type Expander}"
BasedOn="{StaticResource {x:Type Expander}}">
<Setter Property="HeaderTemplate"
Value="{StaticResource verticalHeader}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="ExpandDirection"
Value="Right" />
</Style>
</ItemsControl.Resources>
<ItemsControl.Template>
<ControlTemplate>
<!-- Damn you, StackPanel! -->
<StackPanel Orientation="Horizontal" IsItemsHost="True"/>
</ControlTemplate>
</ItemsControl.Template>
<Expander Header="Exp1">
<TextBlock Text="111111111" Background="Red"/>
</Expander>
<Expander Header="Exp2">
<TextBlock Text="222222222" Background="Blue"/>
</Expander>
<Expander Header="Exp3">
<TextBlock Text="333333333" Background="Green"/>
</Expander>
</ItemsControl>
My first thought is to perform this kind of action with a Behavior. This is some functionality that you can add to existing XAML controls that give you some additional customization.
I've only looked at it for something that's not using an ItemsSource as I used a Grid with Columns etc. But in just a plain grid, you can add a behavior that listens for it's childrens Expanded and Collapsed events like this:
public class ExpanderBehavior : Behavior<Grid>
{
private List<Expander> childExpanders = new List<Expander>();
protected override void OnAttached()
{
//since we are accessing it's children, we have to wait until initialise is complete for it's children to be added
AssociatedObject.Initialized += (gridOvject, e) =>
{
foreach (Expander expander in AssociatedObject.Children)
{
//store this so we can quickly contract other expanders (though we could just access Children again)
childExpanders.Add(expander);
//track expanded events
expander.Expanded += (expanderObject, e2) =>
{
//contract all other expanders
foreach (Expander otherExpander in childExpanders)
{
if (expander != otherExpander && otherExpander.IsExpanded)
{
otherExpander.IsExpanded = false;
}
}
//set width to star for the correct column
int index = Grid.GetColum(expanderObject as Expander);
AssociatedObject.ColumnDefinitions[index].Width = new GridLength(1, GridUnitType.Star);
};
//track Collapsed events
expander.Collapsed += (o2, e2) =>
{
//reset all to auto
foreach (ColumnDefinition colDef in AssociatedObject.ColumnDefinitions)
{
colDef.Width = GridLength.Auto;
}
};
}
};
}
}
Use it like this, note you have to add System.Windows.Interactivity as a reference to your project:
<Window ...
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:local="...">
<Window.Resources>
<DataTemplate x:Key="verticalHeader">
<ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type Expander}}, Path=Header}" />
</DataTemplate>
<Style TargetType="{x:Type Expander}"
BasedOn="{StaticResource {x:Type Expander}}">
<Setter Property="HeaderTemplate"
Value="{StaticResource verticalHeader}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="ExpandDirection"
Value="Right" />
</Style>
<local:ExpanderBehavior x:Key="ExpanderBehavor"/>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<i:Interaction.Behaviors>
<local:ExpanderBehavior/>
</i:Interaction.Behaviors>
<Expander Header="Exp1">
<TextBlock Text="111111111" Background="Red"/>
</Expander>
<Expander Header="Exp2" Grid.Column="1">
<TextBlock Text="222222222" Background="Blue"/>
</Expander>
<Expander Header="Exp3" Grid.Column="2">
<TextBlock Text="333333333" Background="Green"/>
</Expander>
</Grid>
</Window>
The final result:
Edit: Working with ItemsControl - add it to the grid that hosts the items, and add a little to manage the column mapping
public class ItemsSourceExpanderBehavior : Behavior<Grid>
{
private List<Expander> childExpanders = new List<Expander>();
protected override void OnAttached()
{
AssociatedObject.Initialized += (gridOvject, e) =>
{
//since we are accessing it's children, we have to wait until initialise is complete for it's children to be added
for (int i = 0; i < AssociatedObject.Children.Count; i++)
{
Expander expander = AssociatedObject.Children[i] as Expander;
//sort out the grid columns
AssociatedObject.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
Grid.SetColumn(expander, i);
childExpanders.Add(expander);
//track expanded events
expander.Expanded += (expanderObject, e2) =>
{
foreach (Expander otherExpander in childExpanders)
{
if (expander != otherExpander && otherExpander.IsExpanded)
{
otherExpander.IsExpanded = false;
}
}
//set width to auto
int index = AssociatedObject.Children.IndexOf(expanderObject as Expander);
AssociatedObject.ColumnDefinitions[index].Width = new GridLength(1, GridUnitType.Star);
};
//track Collapsed events
expander.Collapsed += (o2, e2) =>
{
foreach (ColumnDefinition colDef in AssociatedObject.ColumnDefinitions)
{
colDef.Width = GridLength.Auto;
}
};
}
};
}
}
Used:
<ItemsControl>
<ItemsControl.Template>
<ControlTemplate>
<Grid IsItemsHost="True">
<i:Interaction.Behaviors>
<local:ItemsSourceExpanderBehavior/>
</i:Interaction.Behaviors>
</Grid>
</ControlTemplate>
</ItemsControl.Template>
<Expander Header="Exp1">
<TextBlock Text="111111111" Background="Red"/>
</Expander>
<Expander Header="Exp2">
<TextBlock Text="222222222" Background="Blue"/>
</Expander>
<Expander Header="Exp3">
<TextBlock Text="333333333" Background="Green"/>
</Expander>
</ItemsControl>
Note, that you'll have to add some logic to manage new/removed children if you have any changes to your ItemsSource!

WPF DataGrid ContentPresenter Binding Errors

I'm using a WPF Datagrid with DataGridTemplateColumns with comboboxes in each cell. At startup, the output window repeats the following message and delays startup about 10 seconds. Something related to ContentPresenter and DataContext not being set (DataItem=null). Please help if you can. Here is the error message:
System.Windows.Data Information: 10 : Cannot retrieve value using the binding and no valid fallback value exists; using default instead. BindingExpression:(no path); DataItem=null; target element is 'ContentPresenter' (Name=''); target property is 'Content' (type 'Object')
It's not technically an error, but it delays the startup nonetheless. Here is a subset of the xaml:
<DataGrid x:Name="grid"
AutoGenerateColumns="False"
CanUserAddRows="True"
IsEnabled="True" Grid.Row="1" Grid.Column="0"
EnableRowVirtualization="False"
HorizontalAlignment="Left"
VerticalAlignment="Center"
ScrollViewer.CanContentScroll="True"
GridLinesVisibility="Vertical"
AreRowDetailsFrozen="True"
HorizontalScrollBarVisibility="Visible"
VerticalScrollBarVisibility="Visible"
SelectionMode="Extended"
HeadersVisibility="All"
Height="750"
VirtualizingStackPanel.VirtualizationMode="Standard"
VirtualizingStackPanel.IsVirtualizing="True"
DataContext="{StaticResource vm}"
ItemsSource="{Binding Source={StaticResource vm}, Path=CorpActionAutoPostConfigs, Mode=TwoWay, IsAsync=False}">
<DataGrid.Columns>
<!-- selecteditembinding: source:enum, dest:JournalType -->
<DataGridTemplateColumn Header="JournalType" x:Name="colJournalType">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox x:Name="cbJournalTypes"
ItemsSource="{Binding Source={StaticResource vm}, Path=JournalTypes, IsAsync=False}"
ItemTemplate="{StaticResource GenericDataTemplate}"
SelectedItem="{Binding Path=JournalTypeCode, Mode=TwoWay, Converter={StaticResource JournalTypeConverter}, IsAsync=False}">
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
...more similar columns follow. I feel like I need to set a Style or ControlTemplate or something but not exactly sure how to proceed.
If I use a ListView/GridView structure, these "errors" do not occur and startup is much faster. But I would prefer to use the DataGrid.
One clue is it seems I get that error for each visible cell that is generated. So I tried to define a style for DataGridCell, that sets the control template for each cell and includes a ContentPresenter binding with a fallback value. Did not resolve the errors.
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="DataGridCell" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Border BorderThickness="{TemplateBinding Border.BorderThickness}"
BorderBrush="{TemplateBinding Border.BorderBrush}"
Background="{TemplateBinding Panel.Background}"
SnapsToDevicePixels="True">
<ContentPresenter x:Name="DataGridCellContentPresenter"
Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content, FallbackValue=null}"
ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}"
ContentStringFormat="{TemplateBinding ContentControl.ContentStringFormat}"
SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
EDIT:
Looking at the Visual Tree, it seems the ContentPresenter I have defined in the ControlTemplate contains yet another ContentPresenter. That ContentPresenter is nameless and I suspect is the source of the binding errors. The parent of that ContentPresenter is the Border. Does anyone know how to define this ContentPresenter in a ControlTemplate so I can add a fallback value?
I can't yet add a screencap of the visual tree, but here is what it looks like:
DataGridCell
ContentPresenter (name=DataGridCellPresenter)
ContentPresenter (unnamed, Border is parent)
A little late to the party, but I managed a work around for this issue - if derive your own custom class from DataGridTemplateColumn you can explicitly set the FallbackValue for the Binding.
Obviously you then have to go through the XAML and replace the required elements.
public sealed class DataGridTemplateColumnEx : DataGridTemplateColumn
{
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
return LoadTemplateContent(false, dataItem, cell);
}
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
return LoadTemplateContent(true, dataItem, cell);
}
private void ChooseCellTemplateAndSelector(bool isEditing, out DataTemplate template, out DataTemplateSelector templateSelector)
{
template = null;
templateSelector = null;
if (isEditing)
{
template = CellEditingTemplate;
templateSelector = CellEditingTemplateSelector;
}
if (template == null && templateSelector == null)
{
template = CellTemplate;
templateSelector = CellTemplateSelector;
}
}
private FrameworkElement LoadTemplateContent(bool isEditing, object dataItem, DataGridCell cell)
{
ChooseCellTemplateAndSelector(isEditing, out var template, out var templateSelector);
if (template != null || templateSelector != null)
{
var contentPresenter = new ContentPresenter();
var binding = new Binding
{
// Explicitly setting this to NULL, stops the binding FallbackValue messages, and therefore improves perf...
FallbackValue = null
};
BindingOperations.SetBinding(contentPresenter, ContentPresenter.ContentProperty, binding);
contentPresenter.ContentTemplate = template;
contentPresenter.ContentTemplateSelector = templateSelector;
return contentPresenter;
}
return null;
}
}

Child elements of scrollviewer preventing scrolling with mouse wheel?

I'm having a problem getting mouse wheel scrolling to work in the following XAML, which I have simplified for clarity:
<ScrollViewer
HorizontalScrollBarVisibility="Visible"
VerticalScrollBarVisibility="Visible"
CanContentScroll="False"
>
<Grid
MouseDown="Editor_MouseDown"
MouseUp="Editor_MouseUp"
MouseMove="Editor_MouseMove"
Focusable="False"
>
<Grid.Resources>
<DataTemplate
DataType="{x:Type local:DataFieldModel}"
>
<Grid
Margin="0,2,2,2"
>
<TextBox
Cursor="IBeam"
MouseDown="TextBox_MouseDown"
MouseUp="TextBox_MouseUp"
MouseMove="TextBox_MouseMove"
/>
</Grid>
</DataTemplate>
</Grid.Resources>
<ListBox
x:Name="DataFieldListBox"
ItemsSource="{Binding GetDataFields}"
SelectionMode="Extended"
Background="Transparent"
Focusable="False"
>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemContainerStyle>
<Style
TargetType="ListBoxItem"
>
<Setter
Property="Canvas.Left"
Value="{Binding dfX}"
/>
<Setter
Property="Canvas.Top"
Value="{Binding dfY}"
/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Grid>
</ScrollViewer>
Visually, the result is an area of some known size where DataFields read from a collection can be represented with TextBoxes which have arbitrary position, size, et cetera. In cases where the ListBox's styled "area" is too large to display all at once, horizontal and vertical scrolling is possible, but only with the scroll bars.
For better ergonomics and sanity, mouse wheel scrolling should be possible, and normally ScrollViewer would handle it automatically, but the ListBox appears to be handing those events such that the parent ScrollViewer never sees them. So far I have only been able to get wheel scrolling working be setting IsHitTestVisible=False for either the ListBox or the parent Grid, but of course none of the child element's mouse events work after that.
What can I do to ensure the ScrollViewer sees mouse wheel events while preserving others for child elements?
Edit: I just learned that ListBox has a built-in ScrollViewer which is probably stealing wheel events from the parent ScrollViewer and that specifying a control template can disable it. I'll update this question if that resolves the problem.
You can also create a behavior and attach it to the parent control (in which the scroll events should bubble through).
// Used on sub-controls of an expander to bubble the mouse wheel scroll event up
public sealed class BubbleScrollEvent : Behavior<UIElement>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.PreviewMouseWheel += AssociatedObject_PreviewMouseWheel;
}
protected override void OnDetaching()
{
AssociatedObject.PreviewMouseWheel -= AssociatedObject_PreviewMouseWheel;
base.OnDetaching();
}
void AssociatedObject_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
e.Handled = true;
var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
AssociatedObject.RaiseEvent(e2);
}
}
<SomePanel>
<i:Interaction.Behaviors>
<viewsCommon:BubbleScrollEvent />
</i:Interaction.Behaviors>
</SomePanel>
Specifying a ControlTemplate for the Listbox which doesn't include a ScrollViewer solves the problem. See this answer and these two MSDN pages for more information:
ControlTemplate
ListBox Styles and Templates
Another way of implementing this, is by creating you own ScrollViewer like this:
public class MyScrollViewer : ScrollViewer
{
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
var parentElement = Parent as UIElement;
if (parentElement != null)
{
if ((e.Delta > 0 && VerticalOffset == 0) ||
(e.Delta < 0 && VerticalOffset == ScrollableHeight))
{
e.Handled = true;
var routedArgs = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
routedArgs.RoutedEvent = UIElement.MouseWheelEvent;
parentElement.RaiseEvent(routedArgs);
}
}
base.OnMouseWheel(e);
}
}
I know it's a little late but I have another solution that worked for me. I switched out my stackpanel/listbox for an itemscontrol/grid. Not sure why the scroll events work properly but they do in my case.
<ScrollViewer VerticalScrollBarVisibility="Auto" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel">
<StackPanel Orientation="Vertical">
<ListBox ItemsSource="{Binding DrillingConfigs}" Margin="0,5,0,0">
<ListBox.ItemTemplate>
<DataTemplate>
became
<ScrollViewer VerticalScrollBarVisibility="Auto" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ItemsControl ItemsSource="{Binding DrillingConfigs}" Margin="0,5,0,0" Grid.Row="0">
<ItemsControl.ItemTemplate>
<DataTemplate>
isHitTestVisible=False in the child works great for me
Edit This isnt a good way to do it

WPF Expand RadPanelBarItem only in available space

I have a RadPanelBar with each RadPanelItem having a list of entities(Different list in each Item). Each item in the List is shown as a GroupBox. With a large number of items the RadPanelBar has to be scrolled in order for the other RadPanelBarItems to be visible. I want it such that the scrollbar appears within each RadPanelBarItem so that all the RadPanelBarItems will be visible on the screen at the same time and if the contents of an item are too long, the user has to scroll only within each RadPanelBarItem.
I'm using the ItemsSource property of each RadPanelBarItem and setting its ItemTemplate to display the GroupBoxes.
Is there a good way to do this, so that everything(Height and such) is kept dynamic?
Thanks!
There seems to be no easy way to do this. I got the following response from Telerik when I asked a similar question:
If I got your case correctly you have several options:
1) Set the size for PanelBarItem. This way you will limit how big they could be. If
you match items summed size to the size of the PanelBar you should
eliminate the clippings.
2) Customize the PanelBar and PanelBarItem control templates in order
to support automatic proportional sizing. In this case you should
remove the ScrollViewer from PanelBar control template and add a
ScrollViewer in the top level PanelBarItem control template (around
the ItemsPresenter). Also you should change RadPanelBar ItemsPanel to
an appropriate panel. Probably. it is going to be a custom panel in
order to measure the items with equal sizes vertically.
I have made a try to do a custom Panel and modifying the control template. I have got it working but it's quite a lot of code, but here goes:
DistributedHeightPanel.cs
This is the custom Panel which do the layout and distributes the available height.
/// <summary>
/// Panel that distributes the available height amongst it's children (like a vertical StackPanel but the children are not allowed to be placed "outside" the parent's visual area).
/// </summary>
public class DistributedHeightPanel : Panel
{
/// <summary>
/// If set to a positive number, no child will get less height than specified.
/// </summary>
public double ItemsMinHeight
{
get { return (double)GetValue(ItemsMinHeightProperty); }
set { SetValue(ItemsMinHeightProperty, value); }
}
public static readonly DependencyProperty ItemsMinHeightProperty =
DependencyProperty.Register("ItemsMinHeight", typeof(double), typeof(DistributedHeightPanel), new UIPropertyMetadata(0.0));
public DistributedHeightPanel()
: base()
{
}
protected override Size MeasureOverride(Size availableSize)
{
List<double> heights = new List<double>();
//Find out how much height each child desire if it was the only child
foreach (UIElement child in InternalChildren)
{
child.Measure(availableSize);
heights.Add(child.DesiredSize.Height);
}
//Calculate ratio
double ratio = GetRatio(availableSize.Height, heights);
//Do the "real" Measure
foreach (UIElement child in InternalChildren)
{
double actualHeight = child.DesiredSize.Height;
if (ratio < 1)
{
//If ratio < 1 then the child can't have all the space it wants, calculate the new height
actualHeight = child.DesiredSize.Height * ratio;
}
if (ItemsMinHeight > 0 && actualHeight < ItemsMinHeight)
{
//If ItemsMinHeight is set and the child is to small, then set the childs height to ItemsMinHeight
actualHeight = ItemsMinHeight;
}
child.Measure(new Size(availableSize.Width, actualHeight));
}
return availableSize;
}
/// <summary>
/// Calculates the ratio for fitting all heights in <paramref name="heightsToDistribute"/> in the total available height (as supplied in <paramref name="availableHeight"/>)
/// </summary>
private double GetRatio(double availableHeight, List<double> heightsToDistribute)
{
//Copy the heights list
List<double> heights = new List<double>(heightsToDistribute);
double desiredTotalHeight = heights.Sum();
//If no height is desired then return 1
if (desiredTotalHeight <= 0)
return 1;
//Calculate ratio
double ratio = availableHeight / desiredTotalHeight;
//We only want to compress so if ratio is higher than 1 return 1
if (ratio > 1)
{
return 1;
}
//Check if heights become too small when the ratio is used
int tooSmallCount = heights.Count(d => d * ratio < ItemsMinHeight);
//If no or all all heights are too small: return the calculated ratio
if (tooSmallCount == 0 || tooSmallCount == heights.Count)
{
return ratio;
}
else
{
//Remove the items which becomes too small and get a ratio without them (they will get ItemsMinHeight)
heights.RemoveAll(d => d * ratio < ItemsMinHeight);
return GetRatio(availableHeight - ItemsMinHeight * tooSmallCount, heights);
}
}
protected override Size ArrangeOverride(Size finalSize)
{
//Arrange all children like a vertical StackPanel
double y = 0;
foreach (UIElement child in InternalChildren)
{
//child.DesiredSize.Height contains the correct value since it was calculated in MeasureOverride
child.Arrange(new Rect(0, y, finalSize.Width, child.DesiredSize.Height));
y += child.DesiredSize.Height;
}
return finalSize;
}
}
MainWindow.xaml
Contains the control template as a style named DistributedHeightRadPanelBarStyle and a RadPanelBar for testing.
<Window x:Class="WpfApplication9.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication9"
Title="MainWindow" Height="350" Width="525" xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation">
<Window.Resources>
<Style x:Key="DistributedHeightRadPanelBarStyle" TargetType="{x:Type telerik:RadPanelBar}">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<local:DistributedHeightPanel ItemsMinHeight="22" /> <!-- 22 is fine for collapsed headers -->
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type telerik:RadPanelBar}">
<Grid>
<telerik:LayoutTransformControl x:Name="transformationRoot" IsTabStop="False">
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}">
<!-- <ScrollViewer x:Name="ScrollViewer" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" HorizontalScrollBarVisibility="Auto" IsTabStop="False" Padding="{TemplateBinding Padding}" VerticalScrollBarVisibility="Auto" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}">-->
<telerik:StyleManager.Theme>
<telerik:Office_BlackTheme/>
</telerik:StyleManager.Theme>
<ItemsPresenter/>
<!--</ScrollViewer>-->
</Border>
</telerik:LayoutTransformControl>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="Orientation" Value="Horizontal">
<Setter Property="LayoutTransform" TargetName="transformationRoot">
<Setter.Value>
<RotateTransform Angle="-90"/>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="Orientation" Value="Vertical"/>
</Style>
</Window.Resources>
<Grid>
<telerik:RadPanelBar Style="{StaticResource ResourceKey=DistributedHeightRadPanelBarStyle}" VerticalAlignment="Top" ExpandMode="Multiple" HorizontalAlignment="Stretch">
<telerik:RadPanelBarItem DropPosition="Inside" Header="A - Colors" IsExpanded="True">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel>
<TextBlock Height="100" Background="AliceBlue" Text="I'm AliceBlue" />
<TextBlock Height="100" Background="AntiqueWhite" Text="I'm AntiqueWhite" />
</StackPanel>
</ScrollViewer>
</telerik:RadPanelBarItem>
<telerik:RadPanelBarItem DropPosition="Inside" Header="B - Colors" IsExpanded="True">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel>
<TextBlock Height="100" Background="Beige" Text="I'm Beige" />
<TextBlock Height="100" Background="Bisque" Text="I'm Bisque" />
</StackPanel>
</ScrollViewer>
</telerik:RadPanelBarItem>
<telerik:RadPanelBarItem DropPosition="Inside" Header="C - Colors">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel>
<TextBlock Height="100" Background="CadetBlue" Text="I'm CadetBlue" />
<TextBlock Height="100" Background="Chartreuse" Text="I'm Chartreuse" />
</StackPanel>
</ScrollViewer>
</telerik:RadPanelBarItem>
</telerik:RadPanelBar>
</Grid>
Maybe this solution is too late for you to use but hopefully someone will find it useful.

Resources