Bind Width property of elements which are horizontally stretched - c#

I have a group box and grid splitter control in a column of the grid. Horizontal Alignment of group box is set to stretch so it occupies all the space when I drag the splitter. All works well.
Now I need to store the value of the group box in a property of the bound object but as soon as I bind the width property it gets stuck it is no longer stretching itself upon stretching the splitter.
I know the reason because now the binded property is responsible for its width and it is not getting changed. But don't know how to make it work. This is my XAML.
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid x:Name="InnerGrid" HorizontalAlignment="Stretch" Height="{Binding ElementName=Control1,Path=ActualHeight}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="200"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<GroupBox Header="{Binding TrackName}" VerticalAlignment="Stretch" Margin="3 0 3 0" HorizontalAlignment="Stretch" />
<GridSplitter Width="5" VerticalAlignment="Stretch" Focusable="False" Background="Gray"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>

Perhaps you're actually interested in binding the ColumnDefinition width, as so:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding Width}" MinWidth="200"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>

As I understand you need read calculated width of the GroupBox. You can use ActualWidth property for this purpose.
Edit:
You can write custom GroupBox and make use of Dependency Properties:
public class MyGroupBox : GroupBox
{
public static readonly DependencyProperty CurrentWidthProperty =
DependencyProperty.Register("CurrentWidth", typeof(double),
typeof(MyGroupBox), new FrameworkPropertyMetadata(0d));
public double CurrentWidth
{
get { return this.ActualWidth; }
set { SetValue(CurrentWidthProperty, value); }
}
}
XAML:
<Window x:Class="FunWithWpfAndXP.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FunWithWp"
Title="Window1" Height="300" Width="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="200"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<local:MyGroupBox CurrentWidth="{Binding Path=myProp}" VerticalAlignment="Stretch" Margin="3 0 3 0" HorizontalAlignment="Stretch"/>
<GridSplitter Width="5" VerticalAlignment="Stretch" Focusable="False" Background="Gray"/>
</Grid>
</Window>

The problem is that your binding is resetting the width changed by the GridSplitter, setting the Mode of the GroupBox Width binding to OneWayToSource should (probably) help you, you'll probably get something like this:
<GroupBox Width="{Binding Path=MyGroupBoxWidth, Mode=OneWayToSource}"/>
MSDN:
OneWayToSource: Updates the source property when the target property changes.
Setting this will cause that the property in your code will be updated but not the other way around

Related

Why does this data binding not change the GridUnitType?

My goal is to save a GridSplitter position for later recall. The splitter is inside a Grid control that has three columns defined thusly:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding GridPanelWidth, Mode=TwoWay}" />
<ColumnDefinition Width="3" /> <!--splitter itself is in this column-->
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
The property GridPanelWidth is defined this way in the view model:
private GridLength _gridPanelWidth = new GridLength(1, GridUnitType.Star);
public GridLength GridPanelWidth
{
get { return _gridPanelWidth; }
set
{
if (_gridPanelWidth != value)
SetProperty(ref _gridPanelWidth, value, () => GridPanelWidth);
}
}
The problem I am having is that when the splitter is moved, the binding updates only the Double (Value) component of the bound property, but not the GridUnitType part of it.
Example: the property defaults to 1*. User drags the splitter and the value becomes 354*, instead of just 354. On restoring the value, then, it's huge (354 times, not 354 pixels).
Why is this happening, and what would you do about it?
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding GridPanelWidth, Mode=TwoWay}" />
<ColumnDefinition Width="4" />
<!--splitter itself is in this column-->
<ColumnDefinition x:Name="RightColumn" Width="2*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border
BorderBrush="Gray"
BorderThickness="1"
Grid.Column="0"
Grid.Row="0"
/>
<GridSplitter
Background="SteelBlue"
ResizeBehavior="PreviousAndNext"
ResizeDirection="Columns"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
ShowsPreview="False"
Grid.Column="1"
Grid.Row="0"
/>
<Border
BorderBrush="Gray"
BorderThickness="1"
Grid.Column="2"
Grid.Row="0"
/>
<StackPanel
Grid.Row="1"
Grid.ColumnSpan="3"
Grid.Column="0"
>
<TextBlock>
<Run>GridPanelWidth: </Run>
<Run Text="{Binding GridPanelWidth.Value, Mode=OneWay}" />
<Run Text="{Binding GridPanelWidth.GridUnitType, Mode=OneWay}" />
</TextBlock>
<TextBlock>
<Run>RightColumn.Width: </Run>
<Run Text="{Binding Width.Value, ElementName=RightColumn, Mode=OneWay}" />
<Run Text="{Binding Width.GridUnitType, ElementName=RightColumn, Mode=OneWay}" />
</TextBlock>
</StackPanel>
</Grid>
Screenshot 1:
Screenshot 2:
Screenshot 3:
Res ipsa loquitor, as far as I'm concerned, but just to be on the safe side:
Because the parent may be resized, the grid splitter changes the ratio between the two columns, while preserving the GridUnitType.Star for each one so that when the parent is resized, the ratio will naturally remain constant. This preserves the intent of the initial Width values in the XAML.
Width.Value for the left column turns out to be identical to the left Border's ActualWidth, and the same holds true for the right column. You'll have to grab both and save the ratio.
Update
I find Grid/GridSplitter a bit overengineered for everyday use when all I want is Yet Another Navigation Pane, so I recently wrote a SplitterControl that has two content properties and sets up the grid and splitter, with styling, in the template. I hadn't gotten around to making the split ratio persistent, so I did that just now.
What I did was rather painful because the control is configurable, and the code isn't all that great, but I can share if you're interested.
The business end is simple:
When a column resizes, set a flag to block recursion and
PaneRatio = _PART_ContentColumn.Width.Value / _PART_NavColumn.Width.Value;
When PaneRatio changes, if it wasn't set by the column size change handler
_PART_NavColumn.Width = new GridLength(1, GridUnitType.Star);
_PART_ContentColumn.Width = new GridLength(PaneRatio, GridUnitType.Star);
In practice, the navigator/content columns can be swapped, or they can be rows instead. Both of those are done by switching templates on a HeaderedContentControl that's a child of the split control template.

How to place any UI element at the end of text that is trimmed

I have a custom control that contains TextBlock and Ellipse.
I want to have this Ellipse placed in a position where the TextBlock ends. If there is lack of space text should be trimmed and Ellipse should remain at its position. I have created very simple sample to illustrate the problem.
MainPage.xaml.cs:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" x:Name="MainGrid" HorizontalAlignment="Stretch">
<Grid Height="50">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<local:TextWithEllipse x:Name="Control1" Grid.Row="0"/>
<local:TextWithEllipse x:Name="Control2" Grid.Row="1"/>
</Grid>
</Grid>
MainPage.cs:
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
SizeChanged += OnSizeChanged;
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
Control1.Text = "One two three four five six seven eight nine ten One two three four five six seven eight nine ten";
Control2.Text = "Short text";
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
Control1.Width = e.NewSize.Width;
Control2.Width = e.NewSize.Width;
}
}
what is done: the TextWithEllipse controls are getting the same width as the app window has and some texts are written to them - short and long one.
Here is how this control looks like:
TextWithEllipse.xaml.cs:
<UserControl
x:Class="TextResize.TextWithEllipse"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<Grid Height="20" HorizontalAlignment="Stretch" Background="MediumPurple">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="20" MinWidth="20"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" HorizontalAlignment="Stretch" x:Name="TestTitle"
TextTrimming="CharacterEllipsis" FontSize="16"/>
<Ellipse Width="20" Height="20" Fill="Red" Grid.Column="1" HorizontalAlignment="Left"/>
</Grid>
TextWithEllipse.cs:
public sealed partial class TextWithEllipse : UserControl
{
public TextWithEllipse()
{
InitializeComponent();
}
public string Text
{
get { return TestTitle.Text; }
set { TestTitle.Text = value; }
}
}
It works OK if the text width is less than controls width. If the text is long and takes more space it is going outside the window, Ellipse is not shown unless you the windows width is extended.
When the first Column width is changed to '*':
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20" MinWidth="20"/>
the situation is than while the text takes more width than window, the text is being trimmed and Ellipse is in the right place. But if the text is short, the Ellipse will not be placed right after the text, but at the end of window.
How can I achieve that?
I played around with your code a bit to find a solution. As you found out setting ColumnDefinition Width="Auto" won't work because it tells the TextBlock that it has as much space as it needs, so it will never trim. So setting ColumnDefinition Width="*" is the right way to go, but as a little detail change the HorizontalAlignment property of the whole Grid to Left, because then the whole Grid will only stretch if it is required (which will happen when you have a lot of text). The whole code would be:
<Grid Height="20" HorizontalAlignment="Left" Background="MediumPurple">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20" MinWidth="20"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" HorizontalAlignment="Stretch" x:Name="TestTitle"
TextTrimming="CharacterEllipsis" FontSize="16"/>
<Ellipse Width="20" Height="20" Fill="Red" Grid.Column="1" HorizontalAlignment="Left"/>
</Grid>
This at least in the XAML Designer gives me the behavior you would like to have.
You may try to use RelativePanel:
<RelativePanel HorizontalAlignment="Stretch" Height="20">
<TextBlock HorizontalAlignment="Stretch" x:Name="TestTitle" TextTrimming="CharacterEllipsis" FontSize="16" Padding="0,0,20,0"/>
<Ellipse Width="20" Height="20" Fill="Red" RelativePanel.AlignRightWith="TestTitle"/>
</RelativePanel>
You also don't need to change width in OnSizeChanged - it will change automatically even without this event.

WPF Grid Column width in DataTemplate does not work

I have DataWindow and UserControls (different ViewModels).
My DataWindow.Xaml:
<catel:DataWindow.Resources>
<DataTemplate DataType="{x:Type viewmodels:MessageViewModel}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20*"/>
<ColumnDefinition Width="80*"/>
</Grid.ColumnDefinitions>
<Views:MessageView Grid.Column="1"/>
</Grid>
</DataTemplate>
</catel:DataWindow.Resources>
<ItemsControl ItemsSource="{Binding Messages}">
My UserControl: MessageView.Xaml
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Border Grid.Row="0" Background="Red">
</Border>
<Border Grid.Row="1">
...
Content
...
</Border>
</Grid>
Messages : ObservableCollection<ViewModelBase>();
My content in UserControl adds to DataWindow dynamically at runtime. If content width in UserControl is more than WindowWidth ColumnWidth (column1 20* and column2 80*) does not work. I see only Grid.Column(80*) and it's width is 100*. What am I doing wrong?
Thanks for help!
It's impossible render an element 80% of windows when its size is more than windows size.
for solving this problem you can make windows size dynamic by removing Windows Width attribute in your code.

WPF XAML and MinWidth and MaxWidth

I have StackPanel in which I wrapped 7 Rectangles all varying widths.
My First rectangle in the Stack, I have a Min and Max width defined and one of the other Rectangles (see below x:Name="ToBeCollapsed") that is Collapsed by default, BUT is made visible in a certain condition in the C#.
My issue is that the first Rectangle does not stretch to the MAX width if the rectangle "ToBeCollasped" is collapsed. My thought was that the first rectangle would fill the space to the MAX of "755" if the collapsed rectangle was collapsed.
Am I wrong in my logic?
My layout is below:
<StackPanel x:Name="RectangleColumns" Width="1840" Orientation="Horizontal">
<Rectangle MinWidth="575" MaxWidth="755" />
<Rectangle Width="315"/>
<Rectangle Width="180" />
<Rectangle Width="180"/>
<!--If collapsed first rectangle should grow to 755. MinWidth + 180-->
<Rectangle x:Name="ToBeCollapsed" Width="180"/>
<Rectangle Width="220"/>
<Rectangle Width="190"/>
</StackPanel>
A grid with column definitions set properly will do the trick. I've tried this myself using some fill colours and smaller widths to demonstrate the point, that first column grows into the space when the fifth column collapses:
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" x:Name="RectangleColumns" HorizontalAlignment="Stretch ">
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="200" MaxWidth="400"/>
<ColumnDefinition MaxWidth="50"/>
<ColumnDefinition MaxWidth="50"/>
<ColumnDefinition MaxWidth="50"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition MaxWidth="50"/>
<ColumnDefinition MaxWidth="50"/>
</Grid.ColumnDefinitions>
<Rectangle Grid.Column="0" Fill="Aqua" MinWidth="200" MaxWidth="400" />
<Rectangle Grid.Column="1" Fill="Blue" Width="50"/>
<Rectangle Grid.Column="2" Fill="Aqua" Width="50" />
<Rectangle Grid.Column="3" Fill="Blue" Width="50"/>
<Rectangle Grid.Column="4" Fill="Pink" Width="300" Name="toBeCollapsed"/>
<Rectangle Grid.Column="5" Fill="Aqua" Width="50"/>
<Rectangle Grid.Column="6" Fill="Blue" Width="50"/>
</Grid>
<Button Grid.Row="1" Content="Toggle Visibility" Name="ButtonToggle" />
</Grid>
Code-behind, just to demonstrate:
public MainWindow()
{
InitializeComponent();
ButtonToggle.Click += ButtonToggleOnClick;
}
private void ButtonToggleOnClick(object sender, RoutedEventArgs routedEventArgs)
{
toBeCollapsed.Visibility = toBeCollapsed.Visibility == Visibility.Collapsed
? Visibility.Visible
: Visibility.Collapsed;
}
Always look behind the schene:
In WPF, every LayoutControl is a Grid - or reproducable with a Grid - but it uses different Column or Row definitions and Alignment.
Stackpanel is a Grid which contains following setup (Horizontal):
Columndefinition for every added controls with Auto size
All added Child controls HorizontalAligment is Center
Additional Columndefinition with * Width
And that's the way the cookies cramble.
Auto sizing distributes space based on the size of the content that is within a column or row.
Which is usually the smallest possible size :/
So +1 for #Bradley Uffner
Use a Grid where Columndefinition of Min/Max Rectangle is *.

How does WPF decide how to render the textblock textwrap?

I'm trying to get my head around how WPF makes decisions when it renders a textblock with wrap enabled.
I have the following code:
<Window x:Class="WpfWrapTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Width="200" Height="200">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="5"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Border Background="Yellow" Grid.Column="0"></Border>
<GridSplitter ResizeDirection="Columns" Grid.Column="1" Width="3" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"></GridSplitter>
<Grid Grid.Column="2" MinWidth="40">
<TextBlock TextWrapping="Wrap">asdflk;jsadlkfjlaskdjflasdkjflk laskdjfl;askjd l;kasjdf l;kjsadf ;lkajsdfl k</TextBlock>
</Grid>
</Grid>
When starting WPF decides to make my textblock larger than the screen and not take this into account for wrapping
Then when I drag the gridsplitter it somehow makes a different decision (probably because the gridsplitter is setting a width of a neighbour control?)
A third weird behaviour in this sample is when you try to drag the gridsplitter more left than it can go (minwidth on column 1 is 5). Then it decides to re-enlarge the textblock outside the visual screenspace.
What is making WPF do one or the other?
Try setting the 3rd column with to "*" instead. "Auto" means that the TextBlock will use as much space as it needs, effectively meaning it doesn't need to wrap.
When you drag the splitter, you are giving the grid column an explicit size, so TextBlock will wrap to fit to that size.
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="5"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Border Background="Yellow" Grid.Column="0"></Border>
<GridSplitter ResizeDirection="Columns" Grid.Column="1" Width="3" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"></GridSplitter>
<Grid Grid.Column="2" MinWidth="40">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBlock TextWrapping="Wrap">asdflk;jsadlkfjlaskdjflasdkjflk laskdjfl;askjd l;kasjdf l;kjsadf ;lkajsdfl k</TextBlock>
</Grid>
</Grid>
</Window>
It's quite simple really... probably too simple for an answer even.
If the TextBlock or a parent of the TextBlock is given a Width that the TextBlock.Text value is longer than, the text will be wrapped. Otherwise, it will not be wrapped.
It really is that simple. As for the other 'weird' symptoms that you spoke about, I can't really help you there... your code didn't show me them. For example, I'm not sure how you can drag a GridSplitter more left than it can go.

Categories

Resources