How to use grid rows and columns with collapsible items? - c#

I have a grid with several items inside, not all of them are visible: some are collapsed. Is there a simple way in WPF to resize the already visible items in the grid when a new item shows up?
Actually my needs are simpler than that: I only have 2 items that must be stacked vertically. Most of the time only one will be visible, but when the second appears I need the first one to resize to a 2/3 of the size of the grid and the second to be placed below filling the remaining place: 1/3.
I feel like I have to play with the Grid.Row properties (and maybe with Grid.RowSpan) but I don't know how to achieve the desired behavior without messing it up in the code-behind.
What I've tried is mixing the auto-size with the LastChildFill property of DockPanel without success.

You could try something like:
<Grid Background="Green">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition>
<RowDefinition.Style>
<Style TargetType="{x:Type RowDefinition}">
<Setter Property="Height"
Value="0.25*" />
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=secondRect,
Path=Visibility}"
Value="Collapsed">
<Setter Property="Height"
Value="0" />
</DataTrigger>
</Style.Triggers>
</Style>
</RowDefinition.Style>
</RowDefinition>
</Grid.RowDefinitions>
<!-- Row 1 -->
<Rectangle Grid.Row="0"
Fill="Blue" />
<!-- Row 2 -->
<Rectangle x:Name="secondRect"
Grid.Row="1"
Fill="Tomato" />
</Grid>
So you're setting a Style.Trigger on the second Grid row to check if the element it contains is Collapsed and if so set it's Height to "0" and if not it's "0.25*" while Grid Row 1 has it's Height as * or "all remaining space" which would tie in with this fine.
Alternate:
You could do it with Grid.RowSpan as well like you mentioned.
<Grid Background="Green">
<Grid.RowDefinitions>
<RowDefinition Height="0.75*" />
<RowDefinition Height="0.25*" />
</Grid.RowDefinitions>
<!-- Row 1 -->
<Rectangle Grid.Row="0"
Fill="Blue">
<Rectangle.Style>
<Style TargetType="{x:Type Rectangle}">
<Setter Property="Grid.RowSpan"
Value="1" />
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=secondRect,
Path=Visibility}"
Value="Collapsed">
<Setter Property="Grid.RowSpan"
Value="2" />
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
<!-- Row 2 -->
<Rectangle x:Name="secondRect"
Grid.Row="1"
Fill="Tomato"
Visibility="Collapsed" />
</Grid>
Now you got the Trigger on the 1'st Row's "element" than the Grid.RowDefinition and when you detect the second row's element becomes Collapsed you switch the first one's RowSpan to 2 else it stays at 1.

Related

Metro Tab Control Style WPF

I would like to achieve a tabcontrol Style like the one In this project (https://github.com/thielj/MetroFramework). Unfortunately the metroframework project only works for winforms.
Can someone help me to get this styled in xaml?
Would help me alot already, if I have the style for the blue/gray line separating tabitem and tabcontent.
Thanks in advance.
First create two ResourceDictionary to store new templates, one for TabControl and one for TabItem and name them TabControlTemplate.xaml and TabItemTemplate.xaml
Create a copy of default templates for controls mentioned above by right clicking on TabControl and TabItem and then choosing Edit Template > Edit a copy.... Then choose your style names to MetroLikeTabControl and MetroLikeTabItem and set target resource dictionaries for each template. Visual studio creates a copy of that template in selected files.
In TabControlTemplate.xaml add this setter tag to control styles:
<Setter Property="ItemContainerStyle" Value="{DynamicResource MetroLikeTabItem}" />
Then change this part of template:
<TabPanel Grid.Row="0" Grid.Column="0" x:Name="HeaderPanel"
Margin="2,2,2,0" Panel.ZIndex="1"
Background="Transparent" IsItemsHost="True" KeyboardNavigation.TabIndex="1" />
to this new one:
<Grid Grid.Row="0" Grid.Column="0">
<Border BorderThickness="0 0 0 2" BorderBrush="Gray" />
<TabPanel x:Name="HeaderPanel" Margin="2,2,2,0" Panel.ZIndex="1"
Background="Transparent" IsItemsHost="True" KeyboardNavigation.TabIndex="1" />
</Grid>
this adds a border with only bottom thickness and makes your TabPanel's border overlap with TabItem's border (Why only bottom border? because I'm implementing what you want when TabControl's TabStripPlacement property is set to Top. You can set triggers to implement all other states.
In TabItemTemlate.xaml set a BorderBrush = "0 0 0 2" for element with name innerBorder and remove Opacity = "0" property.
Then change styles of IsMouseOver = true, IsSelected = true and IsSelected = false (default style of a TabItem) as desired. This is my edited trigger for Selected state that changes content of TabItem and color of Border to Blue.
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsSelected, RelativeSource={RelativeSource Self}}" Value="true" />
<Condition Binding="{Binding TabStripPlacement, RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type TabControl}}}" Value="Top" />
</MultiDataTrigger.Conditions>
<Setter Property="Panel.ZIndex" Value="1" />
<!-- commented line below, because we don't need Select Scale behaviour in metro style anymore -->
<!--<Setter Property="Margin" Value="-2,-2,-2,0" />-->
<Setter TargetName="innerBorder" Property="Opacity" Value="1" />
<Setter TargetName="innerBorder" Property="BorderThickness" Value="0,0,0,2" />
<Setter TargetName="innerBorder" Property="BorderBrush" Value="#0088cc" />
<Setter TargetName="contentPresenter" Property="TextBlock.Foreground" Value="#0088cc" />
<Setter TargetName="mainBorder" Property="BorderThickness" Value="0" />
</MultiDataTrigger>

Using BasedOn and Trigger to manipulate nested property in parent Style

We have a file of standard styles. One style, SectionGroup, we use on all our GroupBox elements. It has a custom template which uses a Border to put an underline below the header, among other things.
On one page, we have a checkbox next to a GroupBox header; when the user unchecks the checkbox, the contents of the GroupBox hide (visibility collapsed) and the header remains. Unfortunately the underline under the header then looks ugly; we also want to hide this.
I've given it my best attempt, so the parent SectionGroup style now looks like this:
<Style x:Key="SectionGroup" TargetType="GroupBox">
<Style.Resources>
<Thickness x:Key="HeaderBorderThickness">0,0,0,1</Thickness>
</Style.Resources>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GroupBox">
<Grid Margin="0">
...
<Border Grid.Row="0" BorderThickness="{DynamicResource HeaderBorderThickness}" >
<TextBlock Text="{Binding Header, RelativeSource={RelativeSource AncestorType=GroupBox}}"/>
</Border>
...
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
By defining the HeaderBorderThickness resource and using it as a DynamicResource, I can override it in my page (as explained in this answer):
<GroupBox>
<GroupBox.Style>
<Style TargetType="GroupBox" BasedOn="{StaticResource SectionGroup}">
<Style.Resources>
<Thickness x:Key="HeaderBorderThickness">0,0,0,0</Thickness>
</Style.Resources>
<!-- TODO triggers here.. -->
</Style>
</GroupBox.Style>
<GroupBox.Header>Section One</GroupBox.Header>
...contents...
</GroupBox>
So indeed, by redefining a Thickness of the same key, the DynamicResource works as expected and there is no underline on the header.
Now I need to toggle it based on a trigger/binding. I'm pretty new to this, but elsewhere in this page I have figured out to do stuff like this:
<Grid Visibility="{Binding Path=FooBoolean, Converter={StaticResource BooleanToVisibility}}">
I think there's a little more magic involved in our viewmodel class (followed the example of existing bindings & properties), but it works.
Now the question is -- how do I bind the boolean value in FooBoolean, to the HeaderBorderThickness resource value? Or what other means can I use to accomplish my goal?
It seems to me that you could do this in a much more WPF way with a DataTrigger, perhaps something like this:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="{Binding Header, RelativeSource={RelativeSource AncestorType=
GroupBox}}"/>
<Rectangle Grid.Row="1" Height="1" Margin="0,2,0,0" Fill="Black">
<Rectangle.Style>
<Style>
<Setter Property="Rectangle.Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{IsChecked, ElementName=YourCheckBox}"
Value="False">
<Setter Property="Rectangle.Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
</Grid>
This method enables you to set the Width, Height, Padding and whatever other properties of the line. If you can't access the CheckBox directly from the Style, then you could try adding a bool property to bind to both the Checkbox.IsChecked property and the DataTrigger.Binding property. Or just manage the Rectangle.Visibility in your own method.

WPF DataTrigger to display and hide grid column XAML

I have an WPF application that contains a grid. The grid is split into 3 columns with the 3rd grid having zero width upon loading.
I have two datagrids in the other two columns. When the selected item in one of the datagrid changes the other datagrid changes it display values, i.e. a master detail template. This all works fine.
There is one value in the datagrid that if selected I wish this 3rd column to changes its width from zero to 2*. I don't know how to do this?
I wish to achieve this through XAML. I have been looking at data triggers & value converters. I written some code below quickly to test. I have read that setting the column to width=0 there is probably higher on the dependency property setting precedence list. Is there anyway to do this or will I need to use code behind?
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="0"/>
</Grid.ColumnDefinitions>
<DataGrid Grid.Column="0"
ItemsSource="{Binding OrderList}"
SelectedItem="{Binding OrderSelected}"
AutoGenerateColumns="True">
</DataGrid>
<TextBox Grid.Column="1" Text="{Binding OrderSelected.Name}">
</TextBox>
<Grid x:Name="columnHideSeek" Grid.Column="2" Background="Blue">
<Grid.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding OrderSelected.Name}" Value="Mark">
<Setter Property="Grid.Width" Value="10"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
</Grid>
</Grid>
Using the correct DataTrigger, this is totally possible. First, add the Trigger to the UI element that you want to change... the ColumnDefinition, not the Grid:
<ColumnDefinition>
<ColumnDefinition.Style>
<Style TargetType="{x:Type ColumnDefinition}">
<Setter Property="Width" Value="10" />
<Style.Triggers>
<DataTrigger Binding="{Binding OrderSelected.Name}" Value="Mark">
<Setter Property="Width" Value="2*" />
</DataTrigger>
</Style.Triggers>
</Style>
</ColumnDefinition.Style>
</ColumnDefinition>
Next, don't set the Width on the ColumnDefinition element, but in the Style instead. Otherwise the Width on the ColumnDefinition element will override the value set from the DataTrigger.

Is there a way to display items of varying width in wpf wrappanel

I'm trying something like a windows 8 tiles and want to display tiles of varying width and/or height. WrapPanel makes each column equal width and height leaving blank spaces around the smaller items. Is there a way or a panel to display items much like a StackPanel where each items can have individual dimensions and wrap like a WrapPanel?
Edit:
This is my ItemsControl. I replaced the Tile DataTemplate with a simple Border and TextBlock. Width is auto and Tiles looks fine except WrapPanel create equal sized cells.
<ItemsControl ItemsSource="{Binding Path=Contents}" HorizontalContentAlignment="Left">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="#B1DBFC" Height="200px" BorderBrush="#404648" Margin="5">
<TextBlock VerticalAlignment="Center" Text="{Binding Path=Name}" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
You can see from the image that the width of each column is the width of widest item.
If I set the width explicitly on the border, things get more ugly.
The behaviour you are looking for is the default behaviour of the WrapPanel as can be seen from the following sample.
<WrapPanel >
<WrapPanel.Resources>
<Style TargetType="{x:Type Rectangle}">
<Setter Property="Width"
Value="80" />
<Setter Property="Height"
Value="80" />
<Setter Property="Margin"
Value="3" />
<Setter Property="Fill"
Value="#4DB4DD" />
</Style>
</WrapPanel.Resources>
<Rectangle Width="150" />
<Rectangle />
<Rectangle />
<Rectangle />
<Rectangle Width="200"/>
<Rectangle />
<Rectangle />
<Rectangle />
<Rectangle Width="220"/>
<Rectangle />
<Rectangle />
</WrapPanel>
Which produces the following result:
As you can see, The width of each item is honoured.
Your problem is caused by setting the orientation of the WrapPanel to Vertical in your template. This is laying out the items from top-to-bottom rather than left-to-right which means that it is the Height property that you need to be setting (or you could revert back to horizontal layout as in my example).
Compare your output to my screenshot where the panel is set to horizontal orientation; each row is the size of the highest Rectangle. Don't believe me? Try setting one of the Rectangle's Height property to a larger value and you will observe that the row size will increase and the Rectangles no longer line up vertically.
Reading your comments I think the best way to get started is to do the following:
Have a WrapPanel that lays out its content horizontally.
Items should have uniform Height.
Constrain the Height of the WrapPanel to a certain size so that you don't get vertical scrollbars.
The height of your WrapPanel should be worked out using the following formula:
((Height of item + Top Margin + Bottom Margin) x Number of rows))
The width of each item also requires a bit of thought so that the panel lays the items out horizontally like the metro interface (lined up rather than staggered).
There are two tile sizes; the small one is 80px wide and the large one is 166px wide.
The width of the large tile is worked out like this:
(item width * 2) + (left margin + right margin)
This ensures the tiles line up correctly.
So now my XAML looks something like this:
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Disabled">
<StackPanel Orientation="Horizontal"
Margin="10,0">
<StackPanel.Resources>
<Style TargetType="{x:Type Rectangle}">
<Setter Property="Width"
Value="80" />
<Setter Property="Height"
Value="80" />
<Setter Property="Margin"
Value="3" />
<Setter Property="Fill"
Value="#4DB4DD" />
</Style>
<Style TargetType="{x:Type WrapPanel}">
<Setter Property="Margin"
Value="0,0,25,0" />
<Setter Property="MaxWidth"
Value="516" />
</Style>
</StackPanel.Resources>
<WrapPanel Height="258">
<Rectangle Width="166" />
<Rectangle />
<Rectangle Width="166" />
<Rectangle />
<Rectangle Width="166" />
<Rectangle />
<Rectangle />
<Rectangle />
<Rectangle />
<Rectangle Width="166" />
<Rectangle />
</WrapPanel>
<WrapPanel Height="258">
<Rectangle />
<Rectangle Width="166" />
<Rectangle />
<Rectangle />
<Rectangle />
<Rectangle />
<Rectangle />
<Rectangle Width="166" />
<Rectangle />
</WrapPanel>
</StackPanel>
</ScrollViewer>
Which produces the following result:
This should give you enough information to start to re-factor this into a full control. If you do this then keep the following in mind:
The layout properties (small item size, large item size, gaps etc) should be calculated rather than hard coded so that if you want to change, for example, the margins, the layout will still work correctly (I.E. the tiles will still line up).
I would limit the number of 'tiles' that can be displayed (in the current example, if the tiles do not fit in the layout they are simply hidden which is probably not what you want).

WPF Change Grid.ColumnSpan on trigger [duplicate]

This question already has answers here:
How to get DataTemplate.DataTrigger to check for greater than or less than?
(4 answers)
Closed 2 years ago.
tl;dr: I want detail views to appear in 2 columns if 2 items are selected, and to span 2 columns if only 1 item is selected.
I have a DockPanel with 2 ListBoxes docked on the left and a 2-column Grid on the right.
When an item is selected in listBox1, I display a details view in Column 0 of the Grid.
When an item is selected in listBox2, I display a details view in Column 1 of the Grid.
<DockPanel>
<StackPanel DockPanel.Dock="Left" Orientation="Horizontal">
<ListBox x:Name="listBox1" ItemsSource="{Binding Items1}" SelectedItem="{Binding SelectedItem1}"/>
<ListBox x:Name="listBox2" ItemsSource="{Binding Items2}" SelectedItem="{Binding SelectedItem2}"/>
</StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ContentControl Grid.Column="0" Content="{Binding SelectedItem1}" ContentTemplateSelector="{StaticResource SelectedItemTemplateSelector}"/>
<ContentControl Grid.Column="1" Content="{Binding SelectedItem2}" ContentTemplateSelector="{StaticResource SelectedItemTemplateSelector}"/>
</Grid>
</DockPanel>
This works fine. However, I'd like to modify it so that the details view will span both grid columns when there is an item selected in one of the ListBoxes but not in the other.
My first thought was to create a couple of DataTriggers that modify the ColumnSpan of the ContentControls based on the SelectedIndex of the ListBoxes.
This works fine for the details view that normally lives in Grid.Column="0". However, it doesn't work for the details view that normally lives in Grid.Column="1" since the original column number is hard-coded:
<ContentControl Grid.Column="0" Content="{Binding SelectedItem1}" ContentTemplateSelector="{StaticResource SelectedItemTemplateSelector}">
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=listBox2, Path=SelectedIndex}" Value="-1">
<Setter Property="Grid.ColumnSpan" Value="2"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
<ContentControl Grid.Column="1" Content="{Binding SelectedItem2}" ContentTemplateSelector="{StaticResource SelectedItemTemplateSelector}">
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=listBox1, Path=SelectedIndex}" Value="-1">
<!--Can't modify Grid.Column since it's hardcoded above!-->
<Setter Property="Grid.Column" Value="0"/>
<Setter Property="Grid.ColumnSpan" Value="2"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
If I could create a DataTrigger that could do a value comparison against ListBox.SelectedIndex (ie. >=0), then I could avoid hard-coding the column numbers in the first place.
Is there any way to do something like this in XAML?
You can always do the hidden element binding trick. the example below, you just change bindme to 2, and the Grids span will change. I do this on all types of stuff you can't animate with a storyboard.
<Rectangle x:Name="bindme" Width="0" Height="0" Tag="1" />
<Grid Grid.ColumnSpan="{Binding Tag, ElementName=bindme}">

Categories