Setting Columns and RowDefinitions in Resource Dictionary [duplicate] - c#

I want to know how can I style a Grid so that I don't need to specify the
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" SharedSizeGroup="SG1"/>
<ColumnDefinition Width="auto" SharedSizeGroup="SG2"/>
</Grid.ColumnDefinitions>
every time?
Thank you very much!
ps: I did try to search on Google first. But I couldn't find any answer. Anyone who find the answer from google could you please tell me what keyword do you use to search? Sometimes I find it hard to determine what keyword use to search.
ps2: I am too lazy, every I just open chrome and type something and search. If nothing found, I conclude nothing found and come here. Is there anyone will search on Google, then find nothing, then open bing.com and search? And find nothing and go to yahoo and search and search?.....

It was always a pet peeve of mine to have to write out the RowDefinitions and ColumnDefinitions, so one day I got tired of it and wrote some attached properties that can be used for this kind of thing.
Now instead of writing my Grid definition like this:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
</Grid>
I can use
<Grid local:GridHelpers.RowCount="6"
local:GridHelpers.StarRows="5"
local:GridHelpers.ColumnCount="4"
local:GridHelpers.StarColumns="1,3">
</Grid>
It only allows for Auto and * sizes, but most of the time that's all I'm using.
It also supports bindings for dynamically sized Grids
<Grid local:GridHelpers.RowCount="{Binding RowCount}"
local:GridHelpers.ColumnCount="{Binding ColumnCount}" />
Here's a copy of the code in case that site ever goes down :
public class GridHelpers
{
#region RowCount Property
/// <summary>
/// Adds the specified number of Rows to RowDefinitions.
/// Default Height is Auto
/// </summary>
public static readonly DependencyProperty RowCountProperty =
DependencyProperty.RegisterAttached(
"RowCount", typeof(int), typeof(GridHelpers),
new PropertyMetadata(-1, RowCountChanged));
// Get
public static int GetRowCount(DependencyObject obj)
{
return (int)obj.GetValue(RowCountProperty);
}
// Set
public static void SetRowCount(DependencyObject obj, int value)
{
obj.SetValue(RowCountProperty, value);
}
// Change Event - Adds the Rows
public static void RowCountChanged(
DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (!(obj is Grid) || (int)e.NewValue < 0)
return;
Grid grid = (Grid)obj;
grid.RowDefinitions.Clear();
for (int i = 0; i < (int)e.NewValue; i++)
grid.RowDefinitions.Add(
new RowDefinition() { Height = GridLength.Auto });
SetStarRows(grid);
}
#endregion
#region ColumnCount Property
/// <summary>
/// Adds the specified number of Columns to ColumnDefinitions.
/// Default Width is Auto
/// </summary>
public static readonly DependencyProperty ColumnCountProperty =
DependencyProperty.RegisterAttached(
"ColumnCount", typeof(int), typeof(GridHelpers),
new PropertyMetadata(-1, ColumnCountChanged));
// Get
public static int GetColumnCount(DependencyObject obj)
{
return (int)obj.GetValue(ColumnCountProperty);
}
// Set
public static void SetColumnCount(DependencyObject obj, int value)
{
obj.SetValue(ColumnCountProperty, value);
}
// Change Event - Add the Columns
public static void ColumnCountChanged(
DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (!(obj is Grid) || (int)e.NewValue < 0)
return;
Grid grid = (Grid)obj;
grid.ColumnDefinitions.Clear();
for (int i = 0; i < (int)e.NewValue; i++)
grid.ColumnDefinitions.Add(
new ColumnDefinition() { Width = GridLength.Auto });
SetStarColumns(grid);
}
#endregion
#region StarRows Property
/// <summary>
/// Makes the specified Row's Height equal to Star.
/// Can set on multiple Rows
/// </summary>
public static readonly DependencyProperty StarRowsProperty =
DependencyProperty.RegisterAttached(
"StarRows", typeof(string), typeof(GridHelpers),
new PropertyMetadata(string.Empty, StarRowsChanged));
// Get
public static string GetStarRows(DependencyObject obj)
{
return (string)obj.GetValue(StarRowsProperty);
}
// Set
public static void SetStarRows(DependencyObject obj, string value)
{
obj.SetValue(StarRowsProperty, value);
}
// Change Event - Makes specified Row's Height equal to Star
public static void StarRowsChanged(
DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (!(obj is Grid) || string.IsNullOrEmpty(e.NewValue.ToString()))
return;
SetStarRows((Grid)obj);
}
#endregion
#region StarColumns Property
/// <summary>
/// Makes the specified Column's Width equal to Star.
/// Can set on multiple Columns
/// </summary>
public static readonly DependencyProperty StarColumnsProperty =
DependencyProperty.RegisterAttached(
"StarColumns", typeof(string), typeof(GridHelpers),
new PropertyMetadata(string.Empty, StarColumnsChanged));
// Get
public static string GetStarColumns(DependencyObject obj)
{
return (string)obj.GetValue(StarColumnsProperty);
}
// Set
public static void SetStarColumns(DependencyObject obj, string value)
{
obj.SetValue(StarColumnsProperty, value);
}
// Change Event - Makes specified Column's Width equal to Star
public static void StarColumnsChanged(
DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (!(obj is Grid) || string.IsNullOrEmpty(e.NewValue.ToString()))
return;
SetStarColumns((Grid)obj);
}
#endregion
private static void SetStarColumns(Grid grid)
{
string[] starColumns =
GetStarColumns(grid).Split(',');
for (int i = 0; i < grid.ColumnDefinitions.Count; i++)
{
if (starColumns.Contains(i.ToString()))
grid.ColumnDefinitions[i].Width =
new GridLength(1, GridUnitType.Star);
}
}
private static void SetStarRows(Grid grid)
{
string[] starRows =
GetStarRows(grid).Split(',');
for (int i = 0; i < grid.RowDefinitions.Count; i++)
{
if (starRows.Contains(i.ToString()))
grid.RowDefinitions[i].Height =
new GridLength(1, GridUnitType.Star);
}
}
}

This is a solution which doesn't require any helper class.
It's possible to set ColumnDefinitions by using an ItemsControl with a Grid as its ItemsPanelTemplate. This is shown in the example below.
<ItemsControl>
<ItemsControl.Resources>
<Style TargetType="ItemsControl">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="40" />
</Grid.ColumnDefinitions>
</Grid>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Style>
</ItemsControl.Resources>
<TextBox Text="First column" />
<TextBox Text="second column" Grid.Column="1" />
</ItemsControl>

Create attached dependency property with change callback to synchronize collection elements:
<Grid>
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="my:GridUtils.ColumnDefinitions">
<Setter.Value>
<my:ColumnDefinitionCollection>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</my:ColumnDefinitionCollection>
</Setter.Value>
</Setter>
</Style>
</Grid.Style>
<Button Content="Button" />
<Button Content="Button" Grid.Column="1" />
</Grid>
Implementation (RowDefinition support omitted as it's basically identical):
public class GridUtils
{
public static readonly DependencyProperty ColumnDefinitionsProperty =
DependencyProperty.RegisterAttached("ColumnDefinitions", typeof (ColumnDefinitionCollection),
typeof (GridUtils),
new PropertyMetadata(default(ColumnDefinitionCollection),
OnColumnDefinitionsChanged));
private static void OnColumnDefinitionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs ev)
{
var grid = (Grid) d;
var oldValue = (ColumnDefinitionCollection) ev.OldValue;
var newValue = (ColumnDefinitionCollection) ev.NewValue;
grid.ColumnDefinitions.Clear();
if (newValue != null)
foreach (var cd in newValue)
grid.ColumnDefinitions.Add(cd);
}
public static void SetColumnDefinitions(Grid element, ColumnDefinitionCollection value)
{
element.SetValue(ColumnDefinitionsProperty, value);
}
public static ColumnDefinitionCollection GetColumnDefinitions(Grid element)
{
return (ColumnDefinitionCollection) element.GetValue(ColumnDefinitionsProperty);
}
}
public class ColumnDefinitionCollection : List<ColumnDefinition> {}

I believe it's not possible because you can't set a style that affects all ColumnDefinition(s).
Grid does not support ControlTemplate, so you can't do it with composition.
The only hack I can think of would be to create a user control with those 2 columns and extend the grid. But that's nasty.

Here is a way:
1) Create a collection with an attached property like this:
public class ColumnDefinitions : Collection<ColumnDefinition>
{
public static readonly DependencyProperty SourceProperty = DependencyProperty.RegisterAttached(
"Source",
typeof(ColumnDefinitions),
typeof(ColumnDefinitions),
new PropertyMetadata(
default(ColumnDefinitions),
OnColumnDefinitionsChanged));
public static void SetSource(Grid element, ColumnDefinitions value)
{
element.SetValue(SourceProperty, value);
}
[AttachedPropertyBrowsableForChildren(IncludeDescendants = false)]
[AttachedPropertyBrowsableForType(typeof(Grid))]
public static ColumnDefinitions GetSource(Grid element)
{
return (ColumnDefinitions)element.GetValue(SourceProperty);
}
private static void OnColumnDefinitionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var grid = (Grid)d;
grid.ColumnDefinitions.Clear();
var columnDefinitions = (ColumnDefinitions)e.NewValue;
if (columnDefinitions == null)
{
return;
}
foreach (var columnDefinition in columnDefinitions)
{
grid.ColumnDefinitions.Add(columnDefinition);
}
}
}
2) Then you can use it as a resource and in a style for grid like this:
Note that x:Shared="False" must be used. If not the same definition will be added to many grids causing WPF to throw.
<UserControl.Resources>
<demo:ColumnDefinitions x:Key="SomeColumnDefinitions" x:Shared="False">
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</demo:ColumnDefinitions>
<Style x:Key="SomeGridStyle" TargetType="{x:Type Grid}">
<Setter Property="demo:ColumnDefinitions.Source" Value="{StaticResource SomeColumnDefinitions}"></Setter>
</Style>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="5"/>
<RowDefinition />
</Grid.RowDefinitions>
<Grid Style="{StaticResource SomeGridStyle}">
<Rectangle Grid.Row="0"
Grid.Column="0"
Width="120"
Fill="Blue" />
<Rectangle Grid.Row="0"
Grid.Column="1"
Fill="Yellow" />
</Grid>
<Grid Grid.Row="2" Style="{StaticResource SomeGridStyle}">
<Rectangle Grid.Row="0"
Grid.Column="0"
Width="120"
Fill="Blue" />
<Rectangle Grid.Row="0"
Grid.Column="1"
Fill="Yellow" />
</Grid>
</Grid>

Related

Collapse Grid Row in WPF

I have created a custom WPF element extended from RowDefinition that should collapse rows in a grid when the Collapsed property of the element is set to True.
It does it by using a converter and a datatrigger in a style to set the height of the row to 0. It is based on this SO Answer.
In the example below, this works perfectly when the grid splitter is over half way up the window. However, when it is less than half way, the rows still collapse, but the first row does not expand. Instead, there is just a white gap where the rows used to be. This can be seen in the image below.
Similarly, if MinHeight or MaxHeight is set on any of the rows that are collapsed, it no longer collapses the row at all. I tried to fix this by adding setters for these properties in the data trigger but it did not fix it.
My question is what can be done differently so that it does not matter about the size of the rows or if MinHeight / MaxHeight are set, it is just able to collapse the rows?
MCVE
MainWindow.xaml.cs
using System;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace RowCollapsibleMCVE
{
public partial class MainWindow : INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private bool isCollapsed;
public bool IsCollapsed
{
get => isCollapsed;
set
{
isCollapsed = value;
OnPropertyChanged();
}
}
}
public class CollapsibleRow : RowDefinition
{
#region Default Values
private const bool COLLAPSED_DEFAULT = false;
private const bool INVERT_COLLAPSED_DEFAULT = false;
#endregion
#region Dependency Properties
public static readonly DependencyProperty CollapsedProperty =
DependencyProperty.Register("Collapsed", typeof(bool), typeof(CollapsibleRow), new PropertyMetadata(COLLAPSED_DEFAULT));
public static readonly DependencyProperty InvertCollapsedProperty =
DependencyProperty.Register("InvertCollapsed", typeof(bool), typeof(CollapsibleRow), new PropertyMetadata(INVERT_COLLAPSED_DEFAULT));
#endregion
#region Properties
public bool Collapsed {
get => (bool)GetValue(CollapsedProperty);
set => SetValue(CollapsedProperty, value);
}
public bool InvertCollapsed {
get => (bool)GetValue(InvertCollapsedProperty);
set => SetValue(InvertCollapsedProperty, value);
}
#endregion
}
public class BoolVisibilityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length > 0 && values[0] is bool collapsed)
{
if (values.Length > 1 && values[1] is bool invert && invert)
{
collapsed = !collapsed;
}
return collapsed ? Visibility.Collapsed : Visibility.Visible;
}
return Visibility.Collapsed;
}
public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
}
MainWindow.xaml
<Window x:Class="RowCollapsibleMCVE.MainWindow"
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"
xmlns:local="clr-namespace:RowCollapsibleMCVE"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<Visibility x:Key="CollapsedVisibilityVal">Collapsed</Visibility>
<local:BoolVisibilityConverter x:Key="BoolVisibilityConverter"/>
<Style TargetType="{x:Type local:CollapsibleRow}">
<Style.Triggers>
<DataTrigger Value="{StaticResource CollapsedVisibilityVal}">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource BoolVisibilityConverter}">
<Binding Path="Collapsed"
RelativeSource="{RelativeSource Self}"/>
<Binding Path="InvertCollapsed"
RelativeSource="{RelativeSource Self}"/>
</MultiBinding>
</DataTrigger.Binding>
<DataTrigger.Setters>
<Setter Property="MinHeight" Value="0"/>
<Setter Property="Height" Value="0"/>
<Setter Property="MaxHeight" Value="0"/>
</DataTrigger.Setters>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<CheckBox Content="Collapse Row"
IsChecked="{Binding IsCollapsed}"/>
<Grid Row="1">
<Grid.RowDefinitions>
<local:CollapsibleRow Height="3*" />
<local:CollapsibleRow Height="Auto" />
<local:CollapsibleRow Collapsed="{Binding IsCollapsed}" Height="*" /> <!-- Using [MaxHeight="300"] breaks this completely -->
</Grid.RowDefinitions>
<StackPanel Background="Red"/>
<GridSplitter Grid.Row="1"
Height="10"
HorizontalAlignment="Stretch">
<GridSplitter.Visibility>
<MultiBinding Converter="{StaticResource BoolVisibilityConverter}" >
<Binding Path="IsCollapsed"/>
</MultiBinding>
</GridSplitter.Visibility>
</GridSplitter>
<StackPanel Background="Blue"
Grid.Row="2">
<StackPanel.Visibility>
<MultiBinding Converter="{StaticResource BoolVisibilityConverter}" >
<Binding Path="IsCollapsed"/>
</MultiBinding>
</StackPanel.Visibility>
</StackPanel>
</Grid>
</Grid>
</Window>
All you need is something to cache the height(s) of the visible row. After that, you no longer need converters or to toggle visibility of contained controls.
CollapsibleRow
public class CollapsibleRow : RowDefinition
{
#region Fields
private GridLength cachedHeight;
private double cachedMinHeight;
#endregion
#region Dependency Properties
public static readonly DependencyProperty CollapsedProperty =
DependencyProperty.Register("Collapsed", typeof(bool), typeof(CollapsibleRow), new PropertyMetadata(false, OnCollapsedChanged));
private static void OnCollapsedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if(d is CollapsibleRow row && e.NewValue is bool collapsed)
{
if(collapsed)
{
if(row.MinHeight != 0)
{
row.cachedMinHeight = row.MinHeight;
row.MinHeight = 0;
}
row.cachedHeight = row.Height;
}
else if(row.cachedMinHeight != 0)
{
row.MinHeight = row.cachedMinHeight;
}
row.Height = collapsed ? new GridLength(0) : row.cachedHeight;
}
}
#endregion
#region Properties
public bool Collapsed
{
get => (bool)GetValue(CollapsedProperty);
set => SetValue(CollapsedProperty, value);
}
#endregion
}
XAML
<Window x:Class="RowCollapsibleMCVE.MainWindow"
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"
xmlns:local="clr-namespace:RowCollapsibleMCVE"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<CheckBox Content="Collapse Row"
IsChecked="{Binding IsCollapsed}"/>
<Grid Row="1">
<Grid.RowDefinitions>
<local:CollapsibleRow Height="3*" MinHeight="0.0001"/>
<local:CollapsibleRow Collapsed="{Binding IsCollapsed}" Height="Auto" />
<local:CollapsibleRow Collapsed="{Binding IsCollapsed}" Height="*" /> <!-- Using [MinHeight="50" MaxHeight="100"] behaves as expected -->
</Grid.RowDefinitions>
<StackPanel Background="Red"/>
<GridSplitter Grid.Row="1" Height="10" HorizontalAlignment="Stretch" />
<StackPanel Background="Blue" Grid.Row="2" />
</Grid>
</Grid>
</Window>
You should have either a MaxHeight on the collapsable row (the third one in our example) or a MinHeight on the non-collapsable row (the first) adjacent to the splitter. This to ensure the star sized row has a size when you put the splitter all the way up and toggle visibility. Only then it will be able to take over the remaining space.
UPDATE
As #Ivan mentioned in his post, the controls that are contained by collapsed rows will still be focusable, allowing users to access them when they shouldn't.
Admittedly, it could be a pain setting the visibility for all controls by hand, especially for large XAMLs. So let's add some custom behavior to sync the collapsed rows with their controls.
The Problem
First, run the example using the code above, then collapse the bottom rows by checking the checkbox. Now, press the TAB key once and use the ARROW UP key to move the GridSplitter. As you can see, even though the splitter isn't visible, the user can still access it.
The Fix
Add a new file Extensions.cs to host the behavior.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using RowCollapsibleMCVE;
namespace Extensions
{
[ValueConversion(typeof(bool), typeof(bool))]
public class BooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return !(bool)value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return Binding.DoNothing;
}
}
public class GridHelper : DependencyObject
{
#region Attached Property
public static readonly DependencyProperty SyncCollapsibleRowsProperty =
DependencyProperty.RegisterAttached(
"SyncCollapsibleRows",
typeof(Boolean),
typeof(GridHelper),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.AffectsRender,
new PropertyChangedCallback(OnSyncWithCollapsibleRows)
));
public static void SetSyncCollapsibleRows(UIElement element, Boolean value)
{
element.SetValue(SyncCollapsibleRowsProperty, value);
}
private static void OnSyncWithCollapsibleRows(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is Grid grid)
{
grid.Loaded += (o,ev) => SetBindingForControlsInCollapsibleRows((Grid)o);
}
}
#endregion
#region Logic
private static IEnumerable<UIElement> GetChildrenFromPanels(IEnumerable<UIElement> elements)
{
Queue<UIElement> queue = new Queue<UIElement>(elements);
while (queue.Any())
{
var uiElement = queue.Dequeue();
if (uiElement is Panel panel)
{
foreach (UIElement child in panel.Children) queue.Enqueue(child);
}
else
{
yield return uiElement;
}
}
}
private static IEnumerable<UIElement> ElementsInRow(Grid grid, int iRow)
{
var rowRootElements = grid.Children.OfType<UIElement>().Where(c => Grid.GetRow(c) == iRow);
if (rowRootElements.Any(e => e is Panel))
{
return GetChildrenFromPanels(rowRootElements);
}
else
{
return rowRootElements;
}
}
private static BooleanConverter MyBooleanConverter = new BooleanConverter();
private static void SyncUIElementWithRow(UIElement uiElement, CollapsibleRow row)
{
BindingOperations.SetBinding(uiElement, UIElement.FocusableProperty, new Binding
{
Path = new PropertyPath(CollapsibleRow.CollapsedProperty),
Source = row,
Converter = MyBooleanConverter
});
}
private static void SetBindingForControlsInCollapsibleRows(Grid grid)
{
for (int i = 0; i < grid.RowDefinitions.Count; i++)
{
if (grid.RowDefinitions[i] is CollapsibleRow row)
{
ElementsInRow(grid, i).ToList().ForEach(uiElement => SyncUIElementWithRow(uiElement, row));
}
}
}
#endregion
}
}
More Testing
Change the XAML to add the behavior and some textboxes (which are also focusable).
<Window x:Class="RowCollapsibleMCVE.MainWindow"
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"
xmlns:local="clr-namespace:RowCollapsibleMCVE"
xmlns:ext="clr-namespace:Extensions"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<CheckBox Content="Collapse Row" IsChecked="{Binding IsCollapsed}"/>
<!-- Set the desired behavior through an Attached Property -->
<Grid ext:GridHelper.SyncCollapsibleRows="True" Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="3*" MinHeight="0.0001" />
<local:CollapsibleRow Collapsed="{Binding IsCollapsed}" Height="Auto" />
<local:CollapsibleRow Collapsed="{Binding IsCollapsed}" Height="*" />
</Grid.RowDefinitions>
<StackPanel Background="Red">
<TextBox Width="100" Margin="40" />
</StackPanel>
<GridSplitter Grid.Row="1" Height="10" HorizontalAlignment="Stretch" />
<StackPanel Grid.Row="2" Background="Blue">
<TextBox Width="100" Margin="40" />
</StackPanel>
</Grid>
</Grid>
</Window>
In the end:
The logic is completely hidden from XAML (clean).
We're still providing flexibility:
For each CollapsibleRow you could bind Collapsed to a different variable.
Rows that don't need the behavior can use base RowDefinition (apply on demand).
UPDATE 2
As #Ash pointed out in the comments, you can use WPF's native caching to store the height values. Resulting in very clean code with autonomous properties, each handling its own => robust code. For example, using the code below you won't be able to move the GridSplitter when rows are collapsed, even without the behavior being applied.
Of course the controls would still be accessible, allowing the user to trigger events. So we'd still need the behavior, but the CoerceValueCallback does provide a consistent linkage between the Collapsed and the various height dependency properties of our CollapsibleRow.
public class CollapsibleRow : RowDefinition
{
public static readonly DependencyProperty CollapsedProperty;
public bool Collapsed
{
get => (bool)GetValue(CollapsedProperty);
set => SetValue(CollapsedProperty, value);
}
static CollapsibleRow()
{
CollapsedProperty = DependencyProperty.Register("Collapsed",
typeof(bool), typeof(CollapsibleRow), new PropertyMetadata(false, OnCollapsedChanged));
RowDefinition.HeightProperty.OverrideMetadata(typeof(CollapsibleRow),
new FrameworkPropertyMetadata(new GridLength(1, GridUnitType.Star), null, CoerceHeight));
RowDefinition.MinHeightProperty.OverrideMetadata(typeof(CollapsibleRow),
new FrameworkPropertyMetadata(0.0, null, CoerceHeight));
RowDefinition.MaxHeightProperty.OverrideMetadata(typeof(CollapsibleRow),
new FrameworkPropertyMetadata(double.PositiveInfinity, null, CoerceHeight));
}
private static object CoerceHeight(DependencyObject d, object baseValue)
{
return (((CollapsibleRow)d).Collapsed) ? (baseValue is GridLength ? new GridLength(0) : 0.0 as object) : baseValue;
}
private static void OnCollapsedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
d.CoerceValue(RowDefinition.HeightProperty);
d.CoerceValue(RowDefinition.MinHeightProperty);
d.CoerceValue(RowDefinition.MaxHeightProperty);
}
}
The sample above is technically wrong.
What it essentially does is that it tries to force the height of the row to be 0, which is not what you want or should do - the problem is that the tab key will go through the controls even if the height is 0, and Narrator will read those controls. Essentially those controls still exist and are completely clickable, functional and accessible just they are not presented on the window, but they can be still accessed in various ways and may affect the work of the application.
Second (and the thing that causes the problems that you describe as you did not describe the problems above though they are essential too and should not be ignored), you have GridSplitter and as said it remains functional even if you force its height to 0 (as explained above). GridSplitter means that at the end of the day you are not in the control of the layout, but the user.
What should be done instead is that you should use the plain RowDefinition and set its height to Auto and then set the Visibility of the content of the rows to Collapsed - of course you may use data binding and the converter.
EDIT: further clarification - in the code above you set the new properties called Collapsed and InvertCollapsed. Just because they are named like that they don't have any effect on the row being collapsed, they could be as well called Property1 and Property2. They are used in the DataTrigger in a fairly strange way - when their value is changed that value is converted to Visibility and then if that converted value is Collapsed the setters that force row height to be 0 are called. So someone played a lot of scenery to make it look like that he is collapsing something, but he does not, he only changes the height which is quite different thing to do. And that's where the problems originate from. I certainly suggest to avoid whole this approach, but if you find it is good for your application the minimal thing you need to do is to avoid that approach for the second row where GridSplitter is set up as if you don't your request becomes impossible.

WPF binding in data template not working for custom class

Working on a ComboBox that displays a list of available tile backgrounds. This is just a simple ComboBox with an ItemSource set to a collection of MapTileBackground objects.
The MapTileBackground class is defined entirely with properties:
public partial class MapTileBackground
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public byte[] Content { get; set; }
public Nullable<int> Color { get; set; }
public int StrokeColor { get; set; }
public byte StrokeThickness { get; set; }
}
which is defined in a separate library and I would prefer to not change it.
I have defined a simple shape extension to draw the background::
public class MapTileBackgroundPreview : Shape
{
public static readonly DependencyProperty SizeProperty = DependencyProperty.Register("Size", typeof(Point), typeof(MapTileBackgroundPreview));
public static readonly DependencyProperty TileBackgroundProperty = DependencyProperty.Register("TileBackground", typeof(MapTileBackground), typeof(MapTileBackgroundPreview));
public MapTileBackgroundPreview()
{
layout = new Hex.Layout(Hex.Orientation.Flat, new Hex.Point(8, 8), new Hex.Point(4, 4));
Size = new Point(8, 8);
TileBackground = null;
}
private Hex.Layout layout;
protected override Geometry DefiningGeometry
{
get
{
var points = layout.HexCorners(0, 0).ToArray();
var path = new PathFigure();
path.StartPoint = points[5].ToWin();
for (var i = 0; i < 6; i++)
path.Segments.Add(new LineSegment(points[i].ToWin(), true));
var geo = new PathGeometry();
geo.Figures.Add(path);
return geo;
}
}
public Point Size
{
get
{
return (Point)GetValue(SizeProperty);
}
set
{
SetValue(SizeProperty, value);
layout.Size = value.ToHex();
layout.Origin = new Hex.Point(layout.Size.X / 2, layout.Size.Y / 2);
}
}
public MapTileBackground TileBackground
{
get
{
return (MapTileBackground)GetValue(TileBackgroundProperty);
}
set
{
SetValue(TileBackgroundProperty, value);
if (value == null)
{
Fill = Brushes.Transparent;
Stroke = Brushes.Black;
StrokeThickness = 1;
}
else
{
Stroke = value.Stroke();
StrokeThickness = value.StrokeThickness();
Fill = value.Fill(layout.Orientation);
}
}
}
}
The layout is just a conversion utility between screen pixel coordinates and a hexagonal system. DefiningGeometry just add 6 line segments of the hex. The TileBackground setter, when given a not null MapTileBackground, updates the Stroke and Fill as the background defines. I've tested this control successfully (outside the combo box data template).
And speaking of:
<DataTemplate x:Key="TileListItemRenderer">
<Grid Width="225">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24"/>
<ColumnDefinition Width="75"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<local:MapTileBackgroundPreview Grid.Row="0" Grid.Column="0" Size="12,12" VerticalAlignment="Center" HorizontalAlignment="Center" TileBackground="{Binding /}"/>
<Label Grid.Row="0" Grid.Column="1" Content="{Binding Name}" HorizontalAlignment="Left" VerticalAlignment="Center" FontWeight="Bold"/>
<TextBlock Grid.Row="0" Grid.Column="2" Text="{Binding Description}" HorizontalAlignment="Stretch" VerticalAlignment="Top" TextWrapping="Wrap" />
</Grid>
</DataTemplate>
So I just create a shape, and two labels, bind the shape to the current MapTileBackground object (combo box ItemSource is a collection of MapTileBackground objects), and the labels to Name and Description.
My problem is the shape is always drawn empty (as in TileBackground is null) and the setter is never invoked. Both the Name Label and Description TextBlock behave as expected (display correct text). And during my debugging attempts, I created an id property on the preview object which in turn invokes the TileBackground Setter and bound it to the Id property (avoid current object bind), again, the TileBackgroundId setter is never invoked. I even added a new label bound to Id to see if that was working and it displays the id as expected. Here are those changes that again did not work. The TileBackgroundId or TileBackground properties are never set when opening the drop down.
<DataTemplate x:Key="TileListItemRenderer">
<Grid Width="225">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24"/>
<ColumnDefinition Width="75"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<local:MapTileBackgroundPreview Grid.Row="0" Grid.Column="0" Size="12,12" VerticalAlignment="Center" HorizontalAlignment="Center" TileBackgroundId="{Binding Id}"/>
<Label Grid.Row="0" Grid.Column="0" Content="{Binding Id}" HorizontalAlignment="Left" VerticalAlignment="Center" FontWeight="Bold"/>
<Label Grid.Row="0" Grid.Column="1" Content="{Binding Name}" HorizontalAlignment="Left" VerticalAlignment="Center" FontWeight="Bold"/>
<TextBlock Grid.Row="0" Grid.Column="2" Text="{Binding Description}" HorizontalAlignment="Stretch" VerticalAlignment="Top" TextWrapping="Wrap" />
</Grid>
</DataTemplate>
public static readonly DependencyProperty TileBackgroundIdProperty = DependencyProperty.Register("TileBackgroundId", typeof(int), typeof(MapTileBackgroundPreview));
public int TileBackgroundId
{
get
{
return (int)GetValue(TileBackgroundIdProperty);
}
set
{
SetValue(TileBackgroundIdProperty, value);
TileBackground = TMapTileBackgroundTool.Get(value);
}
}
TMapTileBackgroundTool.Get() returns the correct object based on Id.
I have also tested instances of MapTileBackgroundPreview setting TileBackgroundId outside the data template.
Any thoughts as to what is going on?
The setter of the CLR wrapper for the dependency property is not supposed to be set as the WPF binding engine calls the GetValue and SetValue methods directly:
Setters not run on Dependency Properties?
Why are .NET property wrappers bypassed at runtime when setting dependency properties in XAML?
The getter and setter of the CLR wrapper property should only call the GetValue and SetValue method respectively.
If you want to do something when the dependency property is set, you should register a callback:
public static readonly DependencyProperty TileBackgroundIdProperty = DependencyProperty.Register("TileBackgroundId", typeof(int), typeof(MapTileBackgroundPreview),
new PropertyMetadata(0, new PropertyChangedCallback(TileBackgroundIdChanged)));
public int TileBackgroundId
{
get
{
return (int)GetValue(TileBackgroundIdProperty);
}
set
{
SetValue(TileBackgroundIdProperty, value);
}
}
private static void TileBackgroundIdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MapTileBackgroundPreview ctrl = (MapTileBackgroundPreview)d;
ctrl.TileBackground = TMapTileBackgroundTool.Get((int)e.NewValue);
}

Making a NxN tic tac toe GUI wpf c#

I am making a NxN tic tac toe game in WPF c#. I want user to enter Rows and Column count(NxN), and my code will generate those number of columns and rows. I can't figure it out, how will I produce that number of rows and columns dynamically. I have posted my XAML code, is there a way that I can loop my XAML code?
thanks
<Grid x:Name="Container">
<!-- First here i want to make N columns-->
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Here i want to make N rows-->
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Then here i want to add that number of buttons in N x N columns and rows -->
<Button x:Name="Button0_0" Grid.Row="0" Grid.Column="0" Click="Button_Click"/>
<Button x:Name="Button0_1" Grid.Row="0" Grid.Column="1" Click="Button_Click" />
<Button x:Name="Button0_2" Grid.Row="0" Grid.Column="2" Click="Button_Click"/>
<Button x:Name="Button1_0" Grid.Row="1" Grid.Column="0" Click="Button_Click"/>
<Button x:Name="Button1_1" Grid.Row="1" Grid.Column="1" Click="Button_Click"/>
<Button x:Name="Button1_2" Grid.Row="1" Grid.Column="2" Click="Button_Click"/>
<Button x:Name="Button2_0" Grid.Row="2" Grid.Column="0" Click="Button_Click"/>
<Button x:Name="Button2_1" Grid.Row="2" Grid.Column="1" Click="Button_Click"/>
<Button x:Name="Button2_2" Grid.Row="2" Grid.Column="2" Click="Button_Click"/>
</Grid>
ItemsControl + UniformGrid as a Panel is a good choice to display a rectangular game board. I have already posted similar solution (How to create and use matrix of (color) boxes C# WPF) but it is missing Rows and Columns binding. Here is a more elaborated example for TicTacToe.
Let's create view model classes:
public class BoardCell : INotifyPropertyChanged
{
private string _sign;
private bool _canSelect = true;
public string Sign
{
get { return _sign; }
set
{
_sign = value;
if (value != null)
CanSelect = false;
OnPropertyChanged();
}
}
public bool CanSelect
{
get { return _canSelect; }
set
{
_canSelect = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public class Board
{
public int Rows { get; set; }
public int Columns { get; set; }
private ObservableCollection<BoardCell> _cells;
public ObservableCollection<BoardCell> Cells
{
get
{
if (_cells == null)
_cells = new ObservableCollection<BoardCell>(Enumerable.Range(0, Rows*Columns).Select(i => new BoardCell()));
return _cells;
}
}
}
and then create a view:
<Grid x:Name="Container">
<Grid.DataContext>
<wpfDemos:Board Rows="8" Columns="8"/>
</Grid.DataContext>
<ItemsControl x:Name="Board" ItemsSource="{Binding Path=Cells}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate >
<UniformGrid Rows="{Binding Path=Rows}" Columns="{Binding Path=Columns}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Path=Sign}"
IsEnabled="{Binding Path=CanSelect}"
Click="CellClick"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
in the code-behind we have one method to allow 2 players make thier moves in turn:
private bool _firstPlayer = true;
private void CellClick(object sender, RoutedEventArgs e)
{
var cell = (sender as Button).DataContext as BoardCell;
cell.Sign = _firstPlayer ? "X" : "O";
_firstPlayer = !_firstPlayer;
// TODO check winner
}
to follow MVVM pattern this method should be wrapped into a command (ICommand), moved to Board class and attached to view via Button.Command binding.
summary: separate logic and presentation. work with data. let controls generate their content based on bindings and provided templates.
I have code for some AttachedProperties on my blog that does exactly this.
XAML code ends up looking like this :
<Grid local:GridHelpers.RowCount="{Binding RowCount}"
local:GridHelpers.ColumnCount="{Binding ColumnCount}" />
Here's the code for the Attached properties, in case the blog post link ever goes down. It also includes some properties to specify your Star rows/columns.
public class GridHelpers
{
#region RowCount Property
/// <summary>
/// Adds the specified number of Rows to RowDefinitions.
/// Default Height is Auto
/// </summary>
public static readonly DependencyProperty RowCountProperty =
DependencyProperty.RegisterAttached(
"RowCount", typeof(int), typeof(GridHelpers),
new PropertyMetadata(-1, RowCountChanged));
// Get
public static int GetRowCount(DependencyObject obj)
{
return (int)obj.GetValue(RowCountProperty);
}
// Set
public static void SetRowCount(DependencyObject obj, int value)
{
obj.SetValue(RowCountProperty, value);
}
// Change Event - Adds the Rows
public static void RowCountChanged(
DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (!(obj is Grid) || (int)e.NewValue < 0)
return;
Grid grid = (Grid)obj;
grid.RowDefinitions.Clear();
for (int i = 0; i < (int)e.NewValue; i++)
grid.RowDefinitions.Add(
new RowDefinition() { Height = GridLength.Auto });
SetStarRows(grid);
}
#endregion
#region ColumnCount Property
/// <summary>
/// Adds the specified number of Columns to ColumnDefinitions.
/// Default Width is Auto
/// </summary>
public static readonly DependencyProperty ColumnCountProperty =
DependencyProperty.RegisterAttached(
"ColumnCount", typeof(int), typeof(GridHelpers),
new PropertyMetadata(-1, ColumnCountChanged));
// Get
public static int GetColumnCount(DependencyObject obj)
{
return (int)obj.GetValue(ColumnCountProperty);
}
// Set
public static void SetColumnCount(DependencyObject obj, int value)
{
obj.SetValue(ColumnCountProperty, value);
}
// Change Event - Add the Columns
public static void ColumnCountChanged(
DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (!(obj is Grid) || (int)e.NewValue < 0)
return;
Grid grid = (Grid)obj;
grid.ColumnDefinitions.Clear();
for (int i = 0; i < (int)e.NewValue; i++)
grid.ColumnDefinitions.Add(
new ColumnDefinition() { Width = GridLength.Auto });
SetStarColumns(grid);
}
#endregion
#region StarRows Property
/// <summary>
/// Makes the specified Row's Height equal to Star.
/// Can set on multiple Rows
/// </summary>
public static readonly DependencyProperty StarRowsProperty =
DependencyProperty.RegisterAttached(
"StarRows", typeof(string), typeof(GridHelpers),
new PropertyMetadata(string.Empty, StarRowsChanged));
// Get
public static string GetStarRows(DependencyObject obj)
{
return (string)obj.GetValue(StarRowsProperty);
}
// Set
public static void SetStarRows(DependencyObject obj, string value)
{
obj.SetValue(StarRowsProperty, value);
}
// Change Event - Makes specified Row's Height equal to Star
public static void StarRowsChanged(
DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (!(obj is Grid) || string.IsNullOrEmpty(e.NewValue.ToString()))
return;
SetStarRows((Grid)obj);
}
#endregion
#region StarColumns Property
/// <summary>
/// Makes the specified Column's Width equal to Star.
/// Can set on multiple Columns
/// </summary>
public static readonly DependencyProperty StarColumnsProperty =
DependencyProperty.RegisterAttached(
"StarColumns", typeof(string), typeof(GridHelpers),
new PropertyMetadata(string.Empty, StarColumnsChanged));
// Get
public static string GetStarColumns(DependencyObject obj)
{
return (string)obj.GetValue(StarColumnsProperty);
}
// Set
public static void SetStarColumns(DependencyObject obj, string value)
{
obj.SetValue(StarColumnsProperty, value);
}
// Change Event - Makes specified Column's Width equal to Star
public static void StarColumnsChanged(
DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (!(obj is Grid) || string.IsNullOrEmpty(e.NewValue.ToString()))
return;
SetStarColumns((Grid)obj);
}
#endregion
private static void SetStarColumns(Grid grid)
{
string[] starColumns =
GetStarColumns(grid).Split(',');
for (int i = 0; i < grid.ColumnDefinitions.Count; i++)
{
if (starColumns.Contains(i.ToString()))
grid.ColumnDefinitions[i].Width =
new GridLength(1, GridUnitType.Star);
}
}
private static void SetStarRows(Grid grid)
{
string[] starRows =
GetStarRows(grid).Split(',');
for (int i = 0; i < grid.RowDefinitions.Count; i++)
{
if (starRows.Contains(i.ToString()))
grid.RowDefinitions[i].Height =
new GridLength(1, GridUnitType.Star);
}
}
}
As Zohar has mentioned you could use the code behind to add child GridColumn's to the Grid.ColumnDefinitions, and then add Button's to the Container Grid. Its an easy approach, but it means using code behind which causes many frowns.
Personally I would rather create a Behaviour and attach it to the Grid. Have your behaviour bind to an integer of Rows and Columns on your viewmodel, and then add ColumnDefinitions and RowDefinitions to the AssociatedObject. Behaviours are a great way to encapsulate reusable features, and eliminates the need for writing code behind.
This chap demonstrates how to create behaviours that bind to your model, and also how to attach them to an element in the view.
http://julmar.com/blog/programming/playing-with-wpf-behaviors-a-watermarktext-behavior/

ListBox does not stay within Window borders when put into a Grid with rows having MinHeight

I have a Grid with two rows sized in 1:3 proportion; the first row has MinHeight set to a non-zero value. When I put a ListBox into the second row, its size is not limited by the window borders:
The problem occurs if MinHeight is applied (that is, if the window is small). If I replace the problematic ListBox with a Button, the problem disappears (buttons always stay within window borders).
MainWindow.xaml
<Window x:Class="WpfGridLayoutMinMax.MainWindow" x:Name="self"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300" Width="300" MinHeight="200" MinWidth="200">
<Control.Resources>
<Style TargetType="ListBox">
<Setter Property="Margin" Value="4"/>
<Setter Property="ItemsSource" Value="{Binding Items, ElementName=self}"/>
</Style>
</Control.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1*" MinHeight="100"/>
<RowDefinition Height="3*"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0"/>
<ListBox Grid.Row="1"/>
</Grid>
</Window>
MainWindow.xaml.cs
using System.Collections.Generic;
using System.Linq;
namespace WpfGridLayoutMinMax
{
public partial class MainWindow
{
public List<int> Items { get; set; }
public MainWindow ()
{
Items = Enumerable.Range(0, 20).ToList();
InitializeComponent();
}
}
}
Question: What causes this problem? How to make ListBox stay within window content area?
This problem appears every time the content is too large (for Button too if you set height larger than remaining space).
Don't know if you like my solution but I've added an additional Grid for measuring the remaining space. Unfortunatly it is not possible to get ActualHeight from second RowDefinition directly (it has no usable value). That's why I've added one more control (the Dummy). Now you can limit ListBox.MaxHeight to Dummy.ActualHeight and it stays within window.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="100" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid x:Name="Dummy" Grid.Row="1" />
<Grid Grid.RowSpan="2">
<Grid.RowDefinitions>
<RowDefinition Height="1*" MinHeight="100"/>
<RowDefinition Height="3*" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0" />
<ListBox Grid.Row="1" MaxHeight="{Binding ActualHeight, ElementName=Dummy}" />
</Grid>
</Grid>
If you set two rows with 1* and 3*, and say that 1* is minimally 100(px) high, then the second row wil minimally be 300(px). Makes sense right? So if you want to keep this ratio, and want to keep the min height for the first row you could set the minheight of you window to 400(or a little bit more).
The problem is caused by the buggy Grid. Its MeasureOverride returns a bigger size than the constraint, even though there's no reason for this.
I've implemented ForceCellSizes attached property which fixes the issue.
public static class GridProps
{
public static readonly DependencyProperty CalculateCellSizesProperty = DependencyProperty.RegisterAttached(
"CalculateCellSizes", typeof(bool), typeof(GridProps),
new PropertyMetadata(false, (o, a) => CalculateCellSizes_OnChanged((Grid)o, a)));
public static readonly DependencyProperty ForceCellSizesProperty = DependencyProperty.RegisterAttached(
"ForceCellSizes", typeof(bool), typeof(GridProps),
new PropertyMetadata(false, (o, a) => ForceCellSizes_OnChanged((Grid)o, a)));
private static readonly DependencyProperty DummyGridProperty = DependencyProperty.RegisterAttached(
"DummyGrid", typeof(Grid), typeof(GridProps),
new PropertyMetadata(null));
private static readonly DependencyPropertyKey RowActualHeightPropertyKey = DependencyProperty.RegisterAttachedReadOnly(
"RowActualHeight", typeof(double), typeof(GridProps),
new PropertyMetadata(0.0));
public static readonly DependencyProperty RowActualHeightProperty = RowActualHeightPropertyKey.DependencyProperty;
private static readonly DependencyPropertyKey ColumnActualWidthPropertyKey = DependencyProperty.RegisterAttachedReadOnly(
"ColumnActualWidth", typeof(double), typeof(GridProps),
new PropertyMetadata(0.0));
public static readonly DependencyProperty ColumnActualWidthProperty = ColumnActualWidthPropertyKey.DependencyProperty;
public static bool GetCalculateCellSizes (Grid grid)
{
return (bool)grid.GetValue(CalculateCellSizesProperty);
}
public static void SetCalculateCellSizes (Grid grid, bool value)
{
grid.SetValue(CalculateCellSizesProperty, value);
}
public static bool GetForceCellSizes (Grid grid)
{
return (bool)grid.GetValue(ForceCellSizesProperty);
}
public static void SetForceCellSizes (Grid grid, bool value)
{
grid.SetValue(ForceCellSizesProperty, value);
}
private static Grid GetDummyGrid (Grid grid)
{
return (Grid)grid.GetValue(DummyGridProperty);
}
private static void SetDummyGrid (Grid grid, Grid value)
{
grid.SetValue(DummyGridProperty, value);
}
public static double GetRowActualHeight (RowDefinition row)
{
return (double)row.GetValue(RowActualHeightProperty);
}
private static void SetRowActualHeight (RowDefinition row, double value)
{
row.SetValue(RowActualHeightPropertyKey, value);
}
public static double GetColumnActualWidth (ColumnDefinition column)
{
return (double)column.GetValue(ColumnActualWidthProperty);
}
private static void SetColumnActualWidth (ColumnDefinition column, double value)
{
column.SetValue(ColumnActualWidthPropertyKey, value);
}
private static void CalculateCellSizes_OnChanged (Grid grid, DependencyPropertyChangedEventArgs args)
{
if ((bool)args.NewValue)
grid.SizeChanged += Grid_OnSizeChanged;
else
grid.SizeChanged -= Grid_OnSizeChanged;
}
private static void Grid_OnSizeChanged (object sender, SizeChangedEventArgs args)
{
var grid = (Grid)sender;
foreach (RowDefinition row in grid.RowDefinitions)
SetRowActualHeight(row, row.ActualHeight);
foreach (ColumnDefinition column in grid.ColumnDefinitions)
SetColumnActualWidth(column, column.ActualWidth);
}
private static void ForceCellSizes_OnChanged (Grid grid, DependencyPropertyChangedEventArgs args)
{
if ((bool)args.NewValue) {
Action initDummyGrid = () => {
Grid parentGrid = (Grid)grid.Parent, dummyGrid = CreateDummyGrid(grid);
parentGrid.Children.Add(dummyGrid);
SetDummyGrid(grid, dummyGrid);
};
if (grid.IsLoaded)
initDummyGrid();
else
grid.Loaded += (o, e) => initDummyGrid();
}
else {
Grid parentGrid = (Grid)grid.Parent, dummyGrid = DestroyDummyGrid(grid);
parentGrid.Children.Remove(dummyGrid);
SetDummyGrid(grid, null);
}
}
private static Grid CreateDummyGrid (Grid grid)
{
var dummyGrid = new Grid { Visibility = Visibility.Hidden };
SetCalculateCellSizes(dummyGrid, true);
foreach (RowDefinition row in grid.RowDefinitions) {
var dummyRow = new RowDefinition { Height = row.Height, MinHeight = row.MinHeight, MaxHeight = row.MaxHeight };
dummyGrid.RowDefinitions.Add(dummyRow);
BindingOperations.SetBinding(row, RowDefinition.HeightProperty,
new Binding { Source = dummyRow, Path = new PropertyPath(RowActualHeightProperty) });
}
foreach (ColumnDefinition column in grid.ColumnDefinitions) {
var dummyColumn = new ColumnDefinition { Width = column.Width, MinWidth = column.MinWidth, MaxWidth = column.MaxWidth };
dummyGrid.ColumnDefinitions.Add(dummyColumn);
BindingOperations.SetBinding(column, ColumnDefinition.WidthProperty,
new Binding { Source = dummyColumn, Path = new PropertyPath(ColumnActualWidthProperty) });
}
return dummyGrid;
}
private static Grid DestroyDummyGrid (Grid grid)
{
Grid dummyGrid = GetDummyGrid(grid);
SetCalculateCellSizes(dummyGrid, false);
foreach (RowDefinition row in grid.RowDefinitions)
BindingOperations.ClearBinding(row, RowDefinition.HeightProperty);
foreach (ColumnDefinition column in grid.ColumnDefinitions)
BindingOperations.ClearBinding(column, ColumnDefinition.WidthProperty);
return dummyGrid;
}
}
Attached properties
The following attached properties are defined in the GridProps class:
Grid.CalculateCellSizes (read/write) — adds bindable RowActualHeight and ColumnActualWidth properties to RowDefinitions and ColumnDefinitions of the grid, respectively.
Grid.ForceCellSizes (read/write) — fixes the problem described in the question.
RowDefinition.RowActualHeight (read-only) — bindable RowDefinition.ActualHeight property. Set CalculateCellSizes on the owner grid to true.
ColumnDefinition.ColumnActualWidth (read-only) — bindable ColumnDefinition.ActualWidth property. Set CalculateCellSizes on the owner grid to true.
How to use
Wrap problematic Grid in an empty Grid.
Set GridProps.ForceCellSizes to true. Example from the question becomes:
<Grid>
<Grid local:GridProps.ForceCellSizes="True">
<Grid.RowDefinitions>
<RowDefinition Height="1*" MinHeight="100"/>
<RowDefinition Height="3*"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0"/>
<ListBox Grid.Row="1"/>
</Grid>
</Grid>
How it works
It adds an empty dummy grid with the same rows and columns as in the original grid, then binds heights and widths of the original grid to actual heights and widths of the dummy grid.
Essentially, the example above becomes:
<Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="{Binding RowDefinitions[0].(local:GridProps.RowActualHeight), ElementName=dummyGrid}" MinHeight="100"/>
<RowDefinition Height="{Binding RowDefinitions[1].(local:GridProps.RowActualHeight), ElementName=dummyGrid}"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0"/>
<ListBox Grid.Row="1"/>
</Grid>
<Grid x:Name="dummyGrid" Visibility="Hidden" local:GridProps.CalculateCellSizes="True">
<Grid.RowDefinitions>
<RowDefinition Height="1*" MinHeight="100"/>
<RowDefinition Height="3*"/>
</Grid.RowDefinitions>
</Grid>
</Grid>

Custom UserControl with ContentControl field

I have a UserControl which acts as a wrapper for a ContentControl, which is simply a title to the ContentControl.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Background="Green" Grid.Row="0">
<TextBlock Text="{Binding Header}" Style="{StaticResource HeaderStyle}" Margin="12, 10, 0, 10" />
</Grid>
<ContentControl HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" Content="{Binding Body}" Grid.Row="1"/>
</Grid>
And here's where I try to use the control:
<gbl:ListHeader Grid.Row="1" Visibility="{Binding HasMovies, Converter={StaticResource VisibilityConverter}}" Header="{Binding Path=LocalizedResources.movie_list_header, Source={StaticResource LocalizedStrings}}" >
<gbl:ListHeader.Body>
<ListBox SelectionChanged="ListBoxContainerSelectionChanged" ItemsSource="{Binding Movies}" ItemContainerStyle="{StaticResource HeaderListBoxItemStyle}">
<ListBox.ItemTemplate>
<DataTemplate>
<gbl:MovieItemControl Header="{Binding MovieTitle}" Description="{Binding FormattedDescription}" Detail="{Binding FormattedDetail}" Opacity="{Binding IsSuppressed, Converter={StaticResource DimIfTrueConverter}}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</gbl:ListHeader.Body>
The DataBinding to the list happens, however nothing displays in the control. I'm guessing that it's still there, but too small to see (undefined h/w).
Is there something that I'm doing wrong? The header shows fine, so the control appears to be working somewhat.
Edit:
Here's the code-behind for ListHeader:
public partial class ListHeader : UserControl
{
private readonly ListHeaderData _data = new ListHeaderData();
public ListHeader()
{
InitializeComponent();
DataContext = _data;
}
public string Header
{
get { return (string)GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
// Using a DependencyProperty as the backing store for Header. This enables animation, styling, binding, etc...
public static readonly DependencyProperty HeaderProperty =
DependencyProperty.Register("Header", typeof(string), typeof(ListHeader), new PropertyMetadata("",HeaderPropertyChanged) );
private static void HeaderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var lh = d as ListHeader;
if (lh != null)
lh._data.Header = e.NewValue as string;
}
public object Body
{
get { return GetValue(BodyProperty); }
set { SetValue(BodyProperty, value); }
}
// Using a DependencyProperty as the backing store for Body. This enables animation, styling, binding, etc...
public static readonly DependencyProperty BodyProperty =
DependencyProperty.Register("Body", typeof(object), typeof(ListHeader), new PropertyMetadata(null, BodyPropertyChanged));
private static void BodyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var lh = d as ListHeader;
if (lh != null)
lh._data.Body = e.NewValue;
}
}
public class ListHeaderData : ViewModelBase
{
public ListHeaderData()
{
if (IsInDesignMode)
{
Header = "Custom Header Goes Here";
Body = new Grid() { Background = new SolidColorBrush(Colors.Yellow) };
}
}
private string _header;
public string Header
{
get { return _header; }
set { _header = value; RaisePropertyChanged("Header"); }
}
private object _body;
public object Body
{
get { return _body; }
set { _body = value; RaisePropertyChanged("Body");}
}
}
In addition to what i said in my comment you appear to bind to your DataContext in the UserControl declaration which is a Bad Thing and the problem of all this.
You appear to want to bind to the properties of the UserControl but you bind directly to the properties of the DataContext which is your ViewModel, hence setting the Body property on an instance in XAML does nothing as the property is sidestepped by the internal binding.
UserControls should for all i know do bindings like this:
<UserControl Name="control" ...>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Background="Green" Grid.Row="0">
<TextBlock Text="{Binding Header, ElementName=control}" Style="{StaticResource HeaderStyle}" Margin="12, 10, 0, 10" />
</Grid>
<ContentControl HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" Content="{Binding Body, ElementName=control}" Grid.Row="1"/>
</Grid>
Get rid of those dependency property changed callbacks and change the property code in ViewModels to this format to make sure it changed:
private int _MyProperty = 0;
public int MyProperty
{
get { return _MyProperty; }
set
{
if (_MyProperty != value)
{
_MyProperty = value;
OnPropertyChanged("MyProperty");
}
}
}

Categories

Resources