How to override MeasureOverride to find the size of ItemsControl - c#

I am developing a UserControl that consists of a block with a heading and a list of items (as ItemsControl). The usercontrol is added dynamically to a canvas. I need to get the actual size of the control (including space taken by ItemsControl) before it gets rendered. I tried overriding MeasureOverride method of the UserControl hoping that the size would be reflected in DesiredSize property. But it is not working.
The XAML is:
<UserControl x:Class="MyTools.MyControl"
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"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid x:Name="LayoutRoot" Background="White">
<Border Name="MainBorder" CornerRadius="5" BorderThickness="2" BorderBrush="Black">
<Grid Name="grid1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" >
<Grid.RowDefinitions>
<RowDefinition Height="34" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Name="titleGrid" Grid.Row="0" Background="#FF727272">
<TextBlock Name="titleText" HorizontalAlignment="Center" Text="{Binding ControlName}" VerticalAlignment="Center" FontSize="13" FontWeight="Bold" Foreground="Beige" />
</Grid>
<Grid Name="gridpr" Grid.Row="1" Background="#12C48F35">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border Name="borderPr" CornerRadius="3" Margin="10" BorderThickness="1" BorderBrush="LightGray" Grid.Row="0">
<Grid Name="gridPr" Background="#FFC1C1C1" MouseLeftButtonUp="gridPr_MouseLeftButtonUp">
<StackPanel>
<TextBlock HorizontalAlignment="Center" Name="txtPr" Text="SubItems" VerticalAlignment="Center" Foreground="#FF584848" FontSize="12" />
<ItemsControl x:Name="pitems" ItemsSource="{Binding MyItems}" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="15,0,0,0">
<TextBlock Text="{Binding MyVal}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</Border>
</Grid>
</Grid>
</Border>
</Grid>
</UserControl>
I am overriding MeasureOverride of the UserControl as shown below:
namespace MyTools
{
public partial class MyControl : UserControl
{
public MyControl()
{
InitializeComponent();
}
public string ControlName { get; set; }
public object MyItems { get; set; }
public class Row
{
public string MyVal { get; set; }
}
protected override Size MeasureOverride(Size availableSize)
{
var desiredSize = base.MeasureOverride(availableSize);
var sideLength = Math.Min(desiredSize.Width, desiredSize.Height);
desiredSize.Width = sideLength;
desiredSize.Height = sideLength;
return desiredSize;
}
}
}
Client Code:
MyControl control1 = new MyControl();
control1.ControlName = "Test Name";
var test = new List<MyControl.Row>(
new MyControl.Row[]
{
new MyControl.Row {MyVal = "Item1"},
new MyControl.Row {MyVal = "Item2"},
new MyControl.Row {MyVal = "Item3"}
});
control1.MyItems = test;
control1.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
MessageBox.Show(control1.DesiredSize.Height.ToString());
canvas1.Children.Add(control1);
I am not getting the actual size using the DesiredSize.Height property from the client. Any idea on how to fix this?

Is your user control defined to change in height to reflect the size of the contents? By default your user control will be a fixed size and not change in height just because the items control has more entries.
I think you need to add your user control inside a grid before then measuring the grid. This is how I measure controls and it seems to work well, even when measuring the control directly does not work as in your case...
MyControl control1 = new MyControl();
... your setup code for control1...
Dim containerGrid As New Grid
containerGrid.Children.Add(control1)
containerGrid.Measure(New Size(Double.MaxValue, Double.MaxValue))
containerGrid.Arrange(New Rect(0, 0, Double.MaxValue, Double.MaxValue))
...now check the grid.ActualWidth and grid.ActualHeight

Try using ActualWidth and ActualHeight properties.
protected override Size MeasureOverride(Size availableSize)
{
var desiredSize = new Size();
var sideLength = Math.Min(ActualWidth, ActualHeight);
desiredSize.Width = sideLength;
desiredSize.Height = sideLength;
return desiredSize;
}

Related

Arranging issue when adding dynamic elements

I am adding dynamic elements to the screen. When the elements fill up the container I am putting them in, I want a vertical scroll bar to appear so that the user can scroll through all of the elements added. The code is performing as expected.
The problem is that when the elements are added to the interface, they are added in a haphazard and weird way. They start in the middle of the grid element, and then shift up. I want them to be added to the top left of the control, then to the right, all the way down the page with equal spacing between the rows. Then when there are too many on the screen, have the scroll bar appear.
Here is all of the code that is running this test. Thanks in advance for any help!
xaml:
<Window x:Class="GPWorkouts.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:GPWorkouts"
mc:Ignorable="d"
Title="MainWindow" Height="370" Width="1000" >
<Grid>
<StackPanel Orientation="Horizontal">
<StackPanel Width="500">
<StackPanel Orientation="Horizontal">
<Button Name="buttonAddNewPlayer" Content="Add New Player" Margin="5" Click="buttonAddNewPlayer_Click" />
</StackPanel>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Hidden" Height="300">
<UniformGrid Columns="2" Name="playerContainer" ScrollViewer.HorizontalScrollBarVisibility="Visible" ScrollViewer.CanContentScroll="True" >
</UniformGrid>
</ScrollViewer>
</StackPanel>
</StackPanel>
</Grid>
</Window>
c# code behind:
using log4net;
using System.Windows;
using System.Windows.Controls;
namespace GPWorkouts
{
public partial class MainWindow : Window
{
private static readonly ILog Log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
int x = 1;
public MainWindow()
{
InitializeComponent();
}
private void buttonAddNewPlayer_Click(object sender, RoutedEventArgs e)
{
Button buttonDyno = new Button()
{
Content = x,
Width = 150,
Height = 32,
Margin = new Thickness(5),
};
x++;
playerContainer.Children.Add(buttonDyno);
}
}
}
the observed effect can be explaned by UniformGrid arrange approach. Buttons start in the middle of the grid element, and then shift up, because UniformGrid has less and less space after each button added.
the effect can be smoothed if you add VerticalAlignment = VerticalAlignment.Top in Button properties.
but I would suggest to use a different panel : WrapPanel
<Grid>
<StackPanel Orientation="Horizontal">
<StackPanel Width="500">
<StackPanel Orientation="Horizontal">
<Button Name="buttonAddNewPlayer"
Margin="5"
Click="buttonAddNewPlayer_Click"
Content="Add New Player" />
</StackPanel>
<ScrollViewer Height="300"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<WrapPanel Name="playerContainer"
Background="Wheat"
Orientation="Horizontal" />
</ScrollViewer>
</StackPanel>
</StackPanel>
</Grid>
private void buttonAddNewPlayer_Click(object sender, RoutedEventArgs e)
{
Button buttonDyno = new Button
{
Content = x,
Width = 150,
Height = 32,
Margin = new Thickness(5),
VerticalAlignment = VerticalAlignment.Top,
HorizontalAlignment = HorizontalAlignment.Center
};
x++;
playerContainer.Children.Add(buttonDyno);
// ensure 2 controls per row
playerContainer.ItemWidth = playerContainer.ActualWidth/2;
}

Show controls in Scrollviewer row along the Scrollbar

So I have this scenario in which I am showing a Grid inside a ScrollViewer.
I want to show a combobox and an image along the scrollbar in a way that it doesn't effect the scrolling functionality,
Something like this:
Currently whenever the scrollviewer becomes visible it appears in a new row, how can I show it along the controls in the same row?
Here is my xaml design:
<DockPanel LastChildFill="True">
<!--Top Panel-->
<Grid DockPanel.Dock="Top">
--GridContent
</Grid>
<!--Bottom Panel-->
<Grid DockPanel.Dock="Bottom">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ComboBox Grid.Column ="0">
</ComboBox>
<Image Grid.Column="1">
</Image>
</Grid>
<ScrollViewer HorizontalScrollBarVisibility="Auto" HorizontalAlignment="Stretch"
VerticalScrollBarVisibility="Auto" VerticalAlignment="Stretch" >
<Grid
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
-- Grid Content
</Grid>
</ScrollViewer>
</DockPanel>
Currently it appears like this :
I haven't looked at doing this in XAML, but you can do it like this in the code behind:
public partial class MainWindow : Window
{
private void IncrementColumn(UIElement element)
{
Grid.SetColumn(element, Grid.GetColumn(element) + 1);
}
public MainWindow()
{
InitializeComponent();
scrollPanel.ApplyTemplate();
var horizontal = scrollPanel.Template.FindName("PART_HorizontalScrollBar", scrollPanel) as ScrollBar;
var vertical = scrollPanel.Template.FindName("PART_VerticalScrollBar", scrollPanel) as ScrollBar;
var presenter = scrollPanel.Template.FindName("PART_ScrollContentPresenter", scrollPanel) as ScrollContentPresenter;
var corner = scrollPanel.Template.FindName("Corner", scrollPanel) as Rectangle;
var grid = corner.Parent as Grid;
grid.ColumnDefinitions.Insert(0, new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Auto) });
IncrementColumn(horizontal);
IncrementColumn(vertical);
IncrementColumn(corner);
Grid.SetColumnSpan(presenter, 2);
var panel = new StackPanel() { Orientation = Orientation.Horizontal };
panel.Children.Add(new ComboBox());
panel.Children.Add(new Image());
Grid.SetRow(panel, 1);
Grid.SetColumn(panel, 0);
grid.Children.Add(panel);
}
}
Here's the XAML to go with it:
<Window x:Class="WpfApplication1.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"
Title="MainWindow" Height="150" Width="525">
<ScrollViewer
Name="scrollPanel"
HorizontalScrollBarVisibility="Visible"
HorizontalAlignment="Stretch"
VerticalScrollBarVisibility="Auto"
VerticalAlignment="Stretch">
<Grid
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Image Source="C:\Users\Public\Pictures\Sample Pictures\Desert.jpg"/>
</Grid>
</ScrollViewer>
</Window>

Layout issues trying to get ListView to show fixed number of items

In a UWP Project I have a ListView that is bound to an ObservableCollection<T> of Person objects. I use a DataTemplate to display the Person object within the ListView. The collection (people) can only contain up to a certain number of people set by _maxPeopleCount.
What I would like to happen is for the ListView to:
Not scroll at all.
Show ALL Person objects as defined by _maxPeopleCount.
Show each Person objects fully, not partially. That is, show each item so that ListViewItem.Height = (Available Height from ItemsPanel ) / _maxPeopleCount.
When there are less than _maxPeopleCount items to start with still show the added items with there Height as defined in (3).
Show the FirstName and LastName of each Person to be as large as possible.
At the moment I can't get my project to do this.
Here is the example code that shows the issue. Simply press the button more than 7 times, as this is the _maxPeopleCount in the code. You will see that 7 people do not get shown. Apologies for the awful UI. It's made to show problem with minimal XAML but be similar to my real project.
Code behind:
public sealed partial class MainPage : Page
{
ObservableCollection<Person> _people = new ObservableCollection<Person>();
int _personCount = 0;
int _maxPeopleCount = 7;
public MainPage()
{
this.InitializeComponent();
this.DataContext = _people;
}
private void btnAddPerson_Click(object sender, RoutedEventArgs e)
{
if (_people.Count == _maxPeopleCount)
{
_people.RemoveAt(_people.Count - 1);
}
_personCount += 1;
_people.Insert(0, new Person($"FirstName {_personCount}", $"LastName {_personCount}"));
}
XAML:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button Grid.Row="0" Grid.Column="0" x:Name="btnAddPerson" Click="btnAddPerson_Click" Content="Add Person" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<ListView BorderBrush="Black" BorderThickness="5" Margin="10" Grid.Row="1" Grid.Column="0" ItemsSource="{Binding}" ScrollViewer.VerticalScrollMode="Disabled" ScrollViewer.VerticalScrollBarVisibility="Hidden" ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.HorizontalScrollMode="Disabled" VerticalAlignment="Stretch" VerticalContentAlignment="Stretch" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel HorizontalAlignment="Stretch"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<Grid x:Name="grdPerson">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="Green" HorizontalAlignment="Stretch">
<Viewbox>
<TextBlock Text="{Binding FirstName}" HorizontalAlignment="Stretch"/>
</Viewbox>
</Border>
<Border Grid.Column="1" Background="Yellow" HorizontalAlignment="Stretch">
<Viewbox>
<TextBlock Text="{Binding LastName}" HorizontalAlignment="Stretch" />
</Viewbox>
</Border>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
PERSON CLASS:
public class Person : INotifyPropertyChanged
{
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
private string _firstName = string.Empty;
public string FirstName
{
get { return _firstName; }
set
{
_firstName = value;
OnPropertyChanged();
}
}
private string _lastName = string.Empty;
public string LastName
{
get { return _lastName; }
set
{
_lastName = value;
OnPropertyChanged();
}
}
protected void OnPropertyChanged([CallerMemberName] string caller = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(caller));
}
public event PropertyChangedEventHandler PropertyChanged;
}
Not scroll at all.
For this, you have already done by ScrollViewer.VerticalScrollMode="Disabled" ScrollViewer.VerticalScrollBarVisibility="Hidden".
Show ALL Person objects as defined by _maxPeopleCount.
Actually you have already bind _maxPeopleCount person records to the ListView, but restricted by the windows size you cannot see them all. The invisible records do exist there. If you stretch the application windows size to let the height be larger by manually dragging you will see the remaining records. If you want to keep all the records always be shown visible you may need to calculate the ListViewItem height by yourself.
Show each Person objects fully, not partially. That is, show each item so that ListViewItem.Height = (Available Height from ItemsPanel ) / _maxPeopleCount.
In your scenario, the height of ListViewItem is calculated automatically since you don't set a fix height for it. As the formula you showed here, we can dynamically bind the height of ListViewItem by ViewTreeHelper class in code behind. Pay attention that windows size change will influence the actual size of the ListView, so we also need to resize the ListViewItem when applicaiton window size changed. When the height of ListViewItem is setting to less than 44, it will not take effects since the default MinHeight of ListViewItem styles and templates is 44, which need to reset. More settings for implementing this requirement please reference the demo below.
When there are less than _maxPeopleCount items to start with still show the added items with there Height as defined in (3).
After calulated, all the ListViewItem will have the same height.
Show the FirstName and LastName of each Person to be as large as possible.
For this you have <Setter Property="HorizontalContentAlignment" Value="Stretch"/> to stretch the horizontal content, please also stretch the vertical content by <Setter Property="VerticalContentAlignment" Value="Stretch"/>. And padding of ListviewItem need to be set to 0.
Updated code as follows which can meet all your above requirements now.
XAML
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" x:Name="gridroot">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button Grid.Row="0" Grid.Column="0" x:Name="btnAddPerson" Click="btnAddPerson_Click" Content="Add Person" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<ListView x:Name="listperson" BorderBrush="Black" BorderThickness="5" Margin="10" Grid.Row="1" Grid.Column="0" ItemsSource="{Binding}" ScrollViewer.VerticalScrollMode="Disabled" ScrollViewer.VerticalScrollBarVisibility="Hidden" ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.HorizontalScrollMode="Disabled" VerticalAlignment="Stretch" VerticalContentAlignment="Stretch" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="MinHeight" Value="20"/>
<Setter Property="Padding" Value="0"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel HorizontalAlignment="Stretch"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<Grid x:Name="grdPerson" Loaded="grdPerson_Loaded" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="Green" HorizontalAlignment="Stretch" >
<Viewbox>
<TextBlock Text="{Binding FirstName}" HorizontalAlignment="Stretch" />
</Viewbox>
</Border>
<Border Grid.Column="1" Background="Yellow" HorizontalAlignment="Stretch">
<Viewbox>
<TextBlock Text="{Binding LastName}" HorizontalAlignment="Stretch" />
</Viewbox>
</Border>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
Code behind
public sealed partial class MainPage : Page
{
ObservableCollection<Person> _people = new ObservableCollection<Person>();
int _personCount = 0;
int _maxPeopleCount = 7;
public MainPage()
{
this.InitializeComponent();
this.DataContext = _people;
Window.Current.SizeChanged += Current_SizeChanged;
}
public void resize()
{
var listpersonheight = listperson.ActualHeight;
IEnumerable<ListViewItem> items = FindVisualChildren<ListViewItem>(listperson);
for (int i = 0; i < items.Count(); i++)
{
foreach (ListViewItem item in items)
{
item.Height = (listpersonheight - 10) / _maxPeopleCount;// BorderThickness size need to be minus.
item.Width = listperson.ActualWidth - 10; //Width also need resize.
}
}
}
private void Current_SizeChanged(object sender, Windows.UI.Core.WindowSizeChangedEventArgs e)
{
resize();
}
private void grdPerson_Loaded(object sender, RoutedEventArgs e)
{
resize();
}
private void btnAddPerson_Click(object sender, RoutedEventArgs e)
{
if (_people.Count == _maxPeopleCount)
{
_people.RemoveAt(_people.Count - 1);
}
_personCount += 1;
_people.Insert(0, new Person($"FirstName {_personCount}", $"LastName {_personCount}"));
}
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T)
{
yield return (T)child;
}
foreach (T childOfChild in FindVisualChildren<T>(child))
{
yield return childOfChild;
}
}
}
}
}
You could always write your own Panel, it's quite easy to do. Here's a UniformStackPanel I wrote which works like a StackPanel except it will stretch each item to fill the available space (regardless of how many items it has).
public class UniformStackPanel : StackPanel
{
protected override Size MeasureOverride(Size availableSize)
{
var childSize = Orientation == Orientation.Horizontal ?
new Size(availableSize.Width / Children.Count, availableSize.Height) :
new Size(availableSize.Width, availableSize.Height / Children.Count);
double alongAxis = 0;
double crossAxis = 0;
foreach (var child in Children)
{
child.Measure(childSize);
if (Orientation == Orientation.Horizontal)
{
alongAxis += child.DesiredSize.Width;
crossAxis = Math.Max(crossAxis, child.DesiredSize.Height);
}
else
{
alongAxis += child.DesiredSize.Height;
crossAxis = Math.Max(crossAxis, child.DesiredSize.Width);
}
}
return Orientation == Orientation.Horizontal ?
new Size(alongAxis, crossAxis) :
new Size(crossAxis, alongAxis);
}
protected override Size ArrangeOverride(Size finalSize)
{
var childSize = Orientation == Orientation.Horizontal ?
new Size(finalSize.Width / Children.Count, finalSize.Height) :
new Size(finalSize.Width, finalSize.Height / Children.Count);
double alongAxis = 0;
foreach (var child in Children)
{
if (Orientation == Orientation.Horizontal)
{
child.Arrange(new Rect(alongAxis, 0, childSize.Width, childSize.Height));
alongAxis += childSize.Width;
}
else
{
child.Arrange(new Rect(0, alongAxis, childSize.Width, childSize.Height));
alongAxis += childSize.Height;
}
}
return finalSize;
}
}
Use it with an ItemsControl like this:
<ItemsControl BorderBrush="Black" BorderThickness="5" Margin="10" Grid.Row="1" ItemsSource="{Binding}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<local:UniformStackPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="RoyalBlue" HorizontalAlignment="Stretch">
<Viewbox>
<TextBlock Text="{Binding FirstName}"/>
</Viewbox>
</Border>
<Border Grid.Column="1" Background="Crimson" HorizontalAlignment="Stretch">
<Viewbox>
<TextBlock Text="{Binding LastName}"/>
</Viewbox>
</Border>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Ok, I think I've cracked it! I changed the ItemsPanelTemplate to a UniformGrid panel that I got from here . Its a UWP version for the one that WPF has, and as such you can set the number of rows and columns. for my example above, Rows = 7 , Columns = 1.
All I changed after that was to add the following additional Setter in the ItemContainerStyle:
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
which fixed the issue of the ListViewItems content not being maximized.
Hope this helps others, and cheers to those who replied with help.

Creating and filling a NxN grid in UWP Xaml

I am trying to create a UWP puzzle game, I want to cut the picture into n parts and then show the pieces in a grid.
My problem is, how to force a certain NxN style. Right now I have to maximize the window in order to see a 3x3 grid, if I shrink either side, it will converge to a 2 column, 1 column grid. Is there a way to handle this?
This is what I have done, I know the RowDefinition is manually right now, until I figure out a better way to do that.
<UserControl
x:Class="PictureSplitter.Views.PictureView"
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">
<GridView ItemsSource="{Binding Splitter.PuzzlePositions}">
<GridView.ItemTemplate>
<DataTemplate>
<Border BorderBrush="Red" BorderThickness="2">
<Grid x:Name="picGrid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Image Source="{Binding Piece.ImageSource}" />
</Grid>
</Border>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</UserControl>
This are two example images:
There are probably couple of ways to do that, here is another one. I've modified the UserControl so that it automatically adjusts items size to show them as square grid, when page size changes and/or collection changes.
The UserControl XAML code:
<UserControl
x:Class="MyControls.MyUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MyControls"
Name="myControl">
<GridView Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" ItemsSource="{Binding ElementName=myControl, Path=Items}"
Width="{Binding ElementName=myControl, Path=CurrentWidth}" HorizontalAlignment="Center"
Height="{Binding Width, RelativeSource={RelativeSource Self}}">
<GridView.ItemContainerStyle>
<Style TargetType="GridViewItem">
<Setter Property="Margin" Value="0"/>
</Style>
</GridView.ItemContainerStyle>
<GridView.ItemTemplate>
<DataTemplate>
<Border Padding="10" Width="{Binding ElementName=myControl, Path=ElementSize}" Height="{Binding ElementName=Width, RelativeSource={RelativeSource Self}}">
<Border BorderBrush="Red" BorderThickness="3">
<Image Source="ms-appx:///Assets/StoreLogo.png" Stretch="UniformToFill"/>
</Border>
</Border>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</UserControl>
UserControl code behind:
public sealed partial class MyUserControl : UserControl, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaiseProperty(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
public IList Items
{
get { return (IList)GetValue(ItemsProperty); }
set { SetValue(ItemsProperty, value); }
}
public static readonly DependencyProperty ItemsProperty =
DependencyProperty.Register("Items", typeof(IList), typeof(MyUserControl),
new PropertyMetadata(0, (s, e) =>
{
if (Math.Sqrt((e.NewValue as IList).Count) % 1 != 0)
Debug.WriteLine("Bad Collection");
}));
public void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (Math.Sqrt(Items.Count) % 1 != 0) Debug.WriteLine("Bad Collection");
RaiseProperty(nameof(ElementSize));
}
private double currentWidth;
public double CurrentWidth
{
get { return currentWidth; }
set { currentWidth = value; RaiseProperty(nameof(CurrentWidth)); RaiseProperty(nameof(ElementSize)); }
}
public double ElementSize => (int)(currentWidth / (int)Math.Sqrt(Items.Count)) - 1;
public MyUserControl()
{
this.InitializeComponent();
}
}
The MainPage XAML:
<Grid>
<local:MyUserControl x:Name="myControl" Items="{Binding MyItems}"/>
<Button Content="Add" Click="Button_Click"/>
</Grid>
MainPage code behind:
public sealed partial class MainPage : Page
{
private ObservableCollection<int> myItems = new ObservableCollection<int> { 1, 2, 3, 4, 5, 6, 7, 8 };
public ObservableCollection<int> MyItems
{
get { return myItems; }
set { myItems = value; }
}
public MainPage()
{
this.InitializeComponent();
DataContext = this;
MyItems.CollectionChanged += myControl.Items_CollectionChanged;
}
protected override Size MeasureOverride(Size availableSize)
{
myControl.CurrentWidth = Math.Min(availableSize.Height, availableSize.Width);
return base.MeasureOverride(availableSize);
}
private void Button_Click(object sender, RoutedEventArgs e) => MyItems.Add(3);
}
The program starts with "Bad Collection" - there are 8 items, so you can't make a square grid from them, but as soon as you click the provided button - the collection's count changes to 9 and the grid should update itself.
It looks like you are doing this by way of MVVM, so I think you need to have a property for your Rows and Columns from your ViewModel. And then you need to have a Converter to supply the coordinate for your pieces .... OR an Attached property.
This will give you an idea:
<Window.Resources>
<System:Int64 x:Key="X">3</System:Int64>
<System:Int64 x:Key="Y">3</System:Int64>
</Window.Resources>
<Grid x:Name="myGrid" Loaded="Grid_Loaded">
// You can bind column and row
// <Button Content="image1" Grid.Column="{Binding column}" Grid.Row="{Binding row}"/>
<Button Content="image1" Grid.Column="0" Grid.Row="0"/>
<Button Content="image2" Grid.Column="1" Grid.Row="0"/>
<Button Content="image3" Grid.Column="2" Grid.Row="0"/>
<Button Content="image4" Grid.Column="0" Grid.Row="1"/>
<Button Content="image5" Grid.Column="1" Grid.Row="1"/>
<Button Content="image6" Grid.Column="2" Grid.Row="1"/>
<Button Content="image7" Grid.Column="0" Grid.Row="2"/>
<Button Content="image8" Grid.Column="1" Grid.Row="2"/>
<Button Content="image9" Grid.Column="2" Grid.Row="2"/>
</Grid>
private void Grid_Loaded(object sender, RoutedEventArgs e)
{
Int64 X = (Int64) this.FindResource("X");
Int64 Y = (Int64) this.FindResource("Y");
for (Int64 i = 0; i < X; i++)
{
ColumnDefinition c = new ColumnDefinition();
myGrid.ColumnDefinitions.Add(c);
}
for (Int64 i = 0; i < (int)Y; i++)
{
RowDefinition r = new RowDefinition();
myGrid.RowDefinitions.Add(r);
}
}
I have used a ListView with GridView as it's View property. And it is working fine.
<ListView x:Name="ImageList" Width="210" Height="210">
<ListView.View>
<GridView>
<GridView.ColumnHeaderContainerStyle>
<Style TargetType="Control">
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
</GridView.ColumnHeaderContainerStyle>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<Image Source="{Binding sq1}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn >
<GridViewColumn.CellTemplate>
<DataTemplate>
<Image Source="{Binding sq2}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn >
<GridViewColumn.CellTemplate>
<DataTemplate>
<Image Source="{Binding sq3}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
var imgBox = new BitmapImage(new Uri(#"/images/cellbkg.jpg", UriKind.Relative));
var source = new[] { new { sq1 = imgBox, sq2 = imgBox, sq3 = imgBox }, new { sq1 = imgBox, sq2 = imgBox, sq3 = imgBox }, new { sq1 = imgBox, sq2 = imgBox, sq3 = imgBox } };
ImageList.ItemsSource = source;
This code produces below output, and don't get collapsed if you reduce window size :
If this is what you want, you can add columns dynamically using below approach. For NxN matrix, you have to add only N columns, binding will take care of rest :
GridView view = (GridView)ImageList.View;
view.Columns.Add(new GridViewColumn());

ScrollViewer ChangeView on SizeChanged

I have DataTemplate which I use in HubSection:
<DataTemplate x:Name="dataTemplate2">
<Grid x:Name="greedy">
<ScrollViewer x:Name="scroller" SizeChanged="ScrollViewer_SizeChanged" Height="{Binding Height,ElementName=greedy}" >
<ItemsControl x:Name="itemsControl"
ItemsSource="{Binding}"
ItemTemplate="{StaticResource dataTemplateDetails}">
<ItemsControl.ItemContainerTransitions>
<TransitionCollection>
<ReorderThemeTransition />
<NavigationThemeTransition />
</TransitionCollection>
</ItemsControl.ItemContainerTransitions>
</ItemsControl>
</ScrollViewer>
</Grid>
</DataTemplate>
In my ItemsControl I have items which can be expandable. What I want to achieve is that when the item will expand to see more details about that item. I want Scrollviewer to scroll down (for amount of changed size of ScrollViewer).
Code behind for SizeChanged event:
private void ScrollViewer_SizeChanged(object sender, SizeChangedEventArgs e)
{
ScrollViewer myScroll = (ScrollViewer)sender;
myScroll.ChangeView(null, myScroll.ScrollableHeight, null, false);
}
What I have done now is not working as my expectation. I scroll down to the end right now. But the thing is it scrolls only when the items extend the size of available view (ScrollBar is shown). Then if I expand another item it doesn't work. If I hide details about item (ScrollBar hides too) and expand it again it will work again.
It's like the SizeChanged event occures only when ScrollViewer is going into action but have infinite height which doesn't change.
I've tried Grid with row set to "*", it changes nothing. Now I try to set height by binding it to height of ItemsControl - still the same behaviour.
Could you help me with the solution, show the path of thinking or enlighten me with some workaround?
EDIT:
I prepared some code to work with to see what happens exactly.
1) Create New Project -> Store Apps (c#) -> Windows Phone 8.1 (Blank App) and name it "scroll"
2) Paste this code into MainPage.xaml
<Page
x:Class="scroll.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:scroll"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="DarkOliveGreen">
<Page.Resources>
<DataTemplate x:Name="dataTemplateDetails">
<Grid Name="grido" Grid.Row="1" Margin="10,10,10,10">
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="Auto" />
<RowDefinition Height="50" />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
<Border Grid.Row="0" Background="Black" CornerRadius="10" Opacity="0.4" />
<Border Grid.Row="1" Background="Black" CornerRadius="10" Opacity="0.3" />
<Border Grid.Row="2" Background="Black" CornerRadius="10" Opacity="0.2" />
<Border Grid.Row="3" Background="Black" CornerRadius="10" Opacity="0.1" />
<TextBlock Grid.Row="0" Text="{Binding Name}" Style="{StaticResource BaseTextBlockStyle}" HorizontalAlignment="Center"/>
<Grid Grid.Row="1" HorizontalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Margin="5,0,5,0" Text="Description:" Style="{StaticResource BaseTextBlockStyle}"/>
<TextBlock Grid.Column="1" Text="{Binding Description}" Style="{StaticResource BaseTextBlockStyle}"/>
</Grid>
<TextBlock Grid.Row="2" Text="Next row" Style="{StaticResource BaseTextBlockStyle}" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="3" Text="Next row" Style="{StaticResource BaseTextBlockStyle}" HorizontalAlignment="Center"/>
</Grid>
</DataTemplate>
<DataTemplate x:Name="dataTemplate2">
<Grid x:Name="greedy" >
<ScrollViewer x:Name="scroller" SizeChanged="ScrollViewer_SizeChanged" VerticalAlignment="Top">
<ItemsControl x:Name="itemsControl"
ItemsSource="{Binding}"
ItemTemplate="{StaticResource dataTemplateDetails}">
<ItemsControl.ItemContainerTransitions>
<TransitionCollection>
<ReorderThemeTransition />
<NavigationThemeTransition />
</TransitionCollection>
</ItemsControl.ItemContainerTransitions>
</ItemsControl>
</ScrollViewer>
</Grid>
</DataTemplate>
</Page.Resources>
<Page.BottomAppBar>
<CommandBar Background="Black" Opacity="0.6" x:Name="myCommandBar">
<AppBarButton Icon="Add" Label="Add" x:Name="AddItem" Click="Add_Click"/>
<AppBarButton Icon="Delete" Label="Delete" x:Name="RemoveItem" Click="Delete_Click"/>
</CommandBar>
</Page.BottomAppBar>
<Grid>
<Hub x:Name="myHub" Header="Test">
<HubSection x:Uid="myDetailsHubsection" x:Name="myDetailsHubsection" Header="Details" DataContext="{Binding Items}" ContentTemplate="{StaticResource dataTemplate2}" />
</Hub>
</Grid>
3) Paste this code into MainPage.xaml.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
namespace scroll
{
public sealed partial class MainPage : Page
{
public static Details dataContextItems;
public MainPage()
{
this.InitializeComponent();
this.NavigationCacheMode = NavigationCacheMode.Required;
dataContextItems = new Details();
}
public class TestItem
{
public string Name { get; set; }
public string Description { get; set; }
public TestItem(string n, string d)
{
Name = n;
Description = d;
}
}
public class Details : INotifyPropertyChanged
{
private ObservableCollection<TestItem> _items;
public ObservableCollection<TestItem> Items
{
set
{
_items = value;
NotifyPropertyChanged("Items");
}
get
{
return _items;
}
}
public Details()
{
_items = new ObservableCollection<TestItem>();
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
this.DataContext = dataContextItems;
}
private void Add_Click(object sender, RoutedEventArgs e)
{
TestItem iAmAnItem = new TestItem("Name of an item", "Long and detailed description of an item");
dataContextItems.Items.Add(iAmAnItem);
}
private void Delete_Click(object sender, RoutedEventArgs e)
{
if (dataContextItems.Items.Count > 0)
dataContextItems.Items.RemoveAt(0);
}
private void ScrollViewer_SizeChanged(object sender, SizeChangedEventArgs e)
{
ScrollViewer myScroll = (ScrollViewer)sender;
myScroll.ChangeView(null, myScroll.ScrollableHeight, null, false);
}
}
}
4) Run the app
5) When you add first two items you can't scroll them but when you add more items you can see that it scrolls down as soon as items "need more space" to be shown and scrollbar occures. But with adding more items it doesn't work. If you delete items and add "third" item again it will scroll down.
I want it to scroll down everytime size of scrollviewer changes (in this case when new item occures but keep in mind it should work when item "extends" in my original solution and there can be few extended items simultaneously).
I've been tinkering with the solution and I finally found a way to do that.
I think the problem is that I didn't understand how ScrollViewer does work. I took the scrolling height as UIElement height hoping for SizeChanged to be fired what isn't truth. ScrollViewer wasn't changing its size because it just took the whole space it could and then just displayed how much content it is in it (It's like ScrollViewer has almost always infinite height unless it's less than actual available view space). With adding first two items SizeChanged event was firing with third one too and then nothing happend. It proves that.
I needed SizeChanged to be fired everytime the size of ScrollViewer (or in this case the Grid) was changing. Solution is very simple but still it needs understanding of how ScrollViewer works - and now it seems so obvious that it will never be greater than available space.
Changes made to make it work:
<DataTemplate x:Name="dataTemplate2">
<ScrollViewer x:Name="scroller" VerticalAlignment="Top" HorizontalAlignment="Stretch" IsEnabled="True" >
<Grid VerticalAlignment="Top" x:Name="greedo" SizeChanged="greedo_SizeChanged">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ItemsControl x:Name="itemsControl"
ItemsSource="{Binding}"
ItemTemplate="{StaticResource dataTemplateDetails}">
<ItemsControl.ItemContainerTransitions>
<TransitionCollection>
<ReorderThemeTransition />
<NavigationThemeTransition />
</TransitionCollection>
</ItemsControl.ItemContainerTransitions>
</ItemsControl>
</Grid>
</ScrollViewer>
</DataTemplate>
and code behind:
private void greedo_SizeChanged(object sender, SizeChangedEventArgs e)
{
Grid takingScroll = (Grid)sender;
ScrollViewer myScroll = (ScrollViewer)takingScroll.Parent;
myScroll.ChangeView(null, myScroll.ScrollableHeight, null, false);
}

Categories

Resources