I'm trying to create a gridview like in the default News app in Windows 10. As far as I know I have to set the ItemHeight an ItemWidth for the VariableSizedWrapGrid. But then it does not stretch the items to fit the full grid width, while the News app does do that as you can see in the pictures below. How do they do that? Is it a special custom control?
As an addition to my previous answer where I show the basic concept here a solution for UWP platform using the VariableSizedWrapPanel
as mentioned in the question:
The main job is done by
<local:MyGridView
ItemsSource="{Binding}"
ItemTemplateSelector="{StaticResource MyGridTemplateSelector}"
MinItemWidth="300" MaxItemWidth="600"
ScrollViewer.VerticalScrollBarVisibility="Hidden">
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<VariableSizedWrapGrid ItemHeight="180" Orientation="Horizontal"/>
</ItemsPanelTemplate>
</GridView.ItemsPanel>
</local:MyGridView>
along with
using System;
using System.Collections.Generic;
using System.Linq;
using Windows.Foundation;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace App1
{
public class MyGridView : GridView
{
private int _columnCount = 1;
private double _itemWidth = 100;
public double MinItemWidth
{
get { return (double) GetValue( MinItemWidthProperty ); }
set { SetValue( MinItemWidthProperty, value ); }
}
// Using a DependencyProperty as the backing store for MinItemWidth. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MinItemWidthProperty =
DependencyProperty.Register( "MinItemWidth", typeof( double ), typeof( MyGridView ), new PropertyMetadata( 100.0 ) );
public double MaxItemWidth
{
get { return (double) GetValue( MaxItemWidthProperty ); }
set { SetValue( MaxItemWidthProperty, value ); }
}
// Using a DependencyProperty as the backing store for MaxItemWidth. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MaxItemWidthProperty =
DependencyProperty.Register( "MaxItemWidth", typeof( double ), typeof( MyGridView ), new PropertyMetadata( 200.0 ) );
private long _itemsPanelPropertyChangedToken;
public MyGridView()
{
_itemsPanelPropertyChangedToken = RegisterPropertyChangedCallback( ItemsPanelProperty, ItemsPanelChangedAsync );
}
private async void ItemsPanelChangedAsync( DependencyObject sender, DependencyProperty dp )
{
UnregisterPropertyChangedCallback( ItemsPanelProperty, _itemsPanelPropertyChangedToken );
await this.Dispatcher.RunIdleAsync( ItemsPanelChangedCallback );
}
private void ItemsPanelChangedCallback( IdleDispatchedHandlerArgs e )
{
var wg = ItemsPanelRoot as VariableSizedWrapGrid;
if (wg != null)
{
wg.ItemWidth = _itemWidth;
}
}
protected override void PrepareContainerForItemOverride( DependencyObject element, object item )
{
var itemIndex = this.Items.IndexOf( item );
element.SetValue( VariableSizedWrapGrid.RowSpanProperty, GetRowSpanByColumnCountAndIndex( _columnCount, itemIndex ) );
element.SetValue( VerticalContentAlignmentProperty, VerticalAlignment.Stretch );
element.SetValue( HorizontalContentAlignmentProperty, HorizontalAlignment.Stretch );
base.PrepareContainerForItemOverride( element, item );
}
private static readonly Dictionary<int, int[]> _rowSpanLayout = new Dictionary<int, int[]>
{
[ 1 ] = new int[] { /* 5 */ 2, 2, 2, 2, 2, /* 6 */ 2, 2, 2, 2, 2, 2, /* 7 */ 2, 2, 2, 2, 2, 2, 2, /* 8 */ 2, 2, 2, 2, 2, 2, 2, 2, /* 9 */ 2, 2, 2, 2, 2, 2, 2, 2, 2 },
[ 2 ] = new int[] { /* 5 */ 2, 1, 2, 2, 1, /* 6 */ 3, 3, 3, 2, 2, 2, /* 7 */ 3, 3, 1, 2, 3, 1, 1, /* 8 */ 2, 3, 2, 3, 3, 3, 3, 1, /* 9 */ 3, 2, 1, 3, 2, 2, 3, 1, 1 },
[ 3 ] = new int[] { /* 5 */ 3, 2, 2, 1, 1, /* 6 */ 2, 3, 2, 3, 3, 2, /* 7 */ 3, 3, 3, 2, 1, 2, 1, /* 8 */ 2, 3, 3, 1, 2, 1, 2, 1, /* 9 */ 3, 3, 3, 1, 2, 1, 3, 3, 2 },
[ 4 ] = new int[] { /* 5 */ 2, 2, 1, 2, 1, /* 6 */ 3, 3, 2, 2, 1, 1, /* 7 */ 3, 2, 2, 2, 1, 1, 1, /* 8 */ 3, 3, 3, 3, 2, 2, 2, 2, /* 9 */ 3, 3, 3, 2, 2, 2, 2, 2, 1 },
[ 5 ] = new int[] { /* 5 */ 2, 2, 2, 2, 2, /* 6 */ 2, 2, 2, 1, 2, 1, /* 7 */ 3, 3, 3, 2, 2, 1, 1, /* 8 */ 3, 3, 2, 2, 2, 1, 1, 1, /* 9 */ 3, 2, 2, 2, 2, 1, 1, 1, 1 },
};
private int GetRowSpanByColumnCountAndIndex( int columnCount, int itemIndex )
{
return _rowSpanLayout[ columnCount ][ itemIndex % 35 ];
}
protected override Size MeasureOverride( Size availableSize )
{
System.Diagnostics.Debug.WriteLine( availableSize );
int columnCount = _columnCount;
double availableWidth = availableSize.Width;
double itemWidth = availableWidth / columnCount;
while ( columnCount > 1 && itemWidth < Math.Min( MinItemWidth, MaxItemWidth ) )
{
columnCount--;
itemWidth = availableWidth / columnCount;
}
while ( columnCount < 5 && itemWidth > Math.Max( MinItemWidth, MaxItemWidth ) )
{
columnCount++;
itemWidth = availableWidth / columnCount;
}
var wg = this.ItemsPanelRoot as VariableSizedWrapGrid;
_itemWidth = itemWidth;
if ( _columnCount != columnCount )
{
_columnCount = columnCount;
if ( wg != null )
{
Update( );
}
}
if ( wg != null )
{
wg.ItemWidth = itemWidth;
}
return base.MeasureOverride( availableSize );
}
// refresh the variablesizedwrapgrid layout
private void Update()
{
if ( !( this.ItemsPanelRoot is VariableSizedWrapGrid ) )
throw new ArgumentException( "ItemsPanel is not VariableSizedWrapGrid" );
int itemIndex = 0;
foreach ( var container in this.ItemsPanelRoot.Children.Cast<GridViewItem>( ) )
{
int rowSpan = GetRowSpanByColumnCountAndIndex( _columnCount, itemIndex );
VariableSizedWrapGrid.SetRowSpan( container, rowSpan );
itemIndex++;
}
this.ItemsPanelRoot.InvalidateMeasure( );
}
}
}
and
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace App1
{
public class MyGridViewTemplateSelector : DataTemplateSelector
{
public DataTemplate Small { get; set; }
public DataTemplate Medium { get; set; }
public DataTemplate Large { get; set; }
protected override DataTemplate SelectTemplateCore( object item, DependencyObject container )
{
var rowSpan = container.GetValue( VariableSizedWrapGrid.RowSpanProperty );
int index;
try
{
dynamic model = item;
index = model.Index;
}
catch ( Exception )
{
index = -1;
}
long token = 0;
DependencyPropertyChangedCallback lambda = ( sender, dp ) =>
{
container.UnregisterPropertyChangedCallback( VariableSizedWrapGrid.RowSpanProperty, token );
var cp = (ContentControl) container;
cp.ContentTemplateSelector = null;
cp.ContentTemplateSelector = this;
};
token = container.RegisterPropertyChangedCallback( VariableSizedWrapGrid.RowSpanProperty, lambda );
switch ( rowSpan )
{
case 1:
return Small;
case 2:
return Medium;
case 3:
return Large;
default:
throw new InvalidOperationException( );
}
}
private void Foo( DependencyObject sender, DependencyProperty dp )
{
throw new NotImplementedException( );
}
}
}
To complete here the other files
<Page
x:Class="App1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:App1"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Page.Resources>
<DataTemplate x:Key="Small">
<Grid Margin="5">
<Grid.Background>
<SolidColorBrush Color="{Binding Path=Color}"/>
</Grid.Background>
<StackPanel VerticalAlignment="Top">
<StackPanel.Background>
<SolidColorBrush Color="White" Opacity="0.75"/>
</StackPanel.Background>
<TextBlock FontSize="15" Margin="10">
<Run Text="{Binding Path=Index}"/>. <Run Text="{Binding Path=Name}"/>
</TextBlock>
<TextBlock Text="Small" TextAlignment="Center"/>
</StackPanel>
</Grid>
</DataTemplate>
<DataTemplate x:Key="Medium">
<Grid Margin="5">
<Grid.Background>
<SolidColorBrush Color="{Binding Path=Color}"/>
</Grid.Background>
<StackPanel VerticalAlignment="Top">
<StackPanel.Background>
<SolidColorBrush Color="White" Opacity="0.75"/>
</StackPanel.Background>
<TextBlock FontSize="15" Margin="10">
<Run Text="{Binding Path=Index}"/>. <Run Text="{Binding Path=Name}"/>
</TextBlock>
<TextBlock Text="Medium" TextAlignment="Center"/>
</StackPanel>
</Grid>
</DataTemplate>
<DataTemplate x:Key="Large">
<Grid Margin="5">
<Grid.Background>
<SolidColorBrush Color="{Binding Path=Color}"/>
</Grid.Background>
<StackPanel VerticalAlignment="Top">
<StackPanel.Background>
<SolidColorBrush Color="White" Opacity="0.75"/>
</StackPanel.Background>
<TextBlock FontSize="15" Margin="10">
<Run Text="{Binding Path=Index}"/>. <Run Text="{Binding Path=Name}"/>
</TextBlock>
<TextBlock Text="Large" TextAlignment="Center"/>
</StackPanel>
</Grid>
</DataTemplate>
<local:MyGridViewTemplateSelector x:Key="MyGridTemplateSelector"
Small="{StaticResource Small}"
Medium="{StaticResource Medium}"
Large="{StaticResource Large}"/>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="48"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="48"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- top left section -->
<Border Background="#D13438">
</Border>
<!-- top bar -->
<Border Grid.Column="1" Grid.Row="0" Padding="5" Background="#F2F2F2">
<TextBlock Text="MenuBar" VerticalAlignment="Center"/>
</Border>
<!-- left bar -->
<Border Grid.Column="0" Grid.Row="1" Width="48" Background="#2B2B2B">
</Border>
<!-- content -->
<Border Grid.Column="1" Grid.Row="1" Background="#E6E6E6">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="48"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Padding="5" Background="#F2F2F2">
<TextBlock Text="SectionBar" VerticalAlignment="Center"/>
</Border>
<ScrollViewer Grid.Row="1">
<Border Margin="7,7,10,7">
<!-- the wrapped news items -->
<local:MyGridView ItemsSource="{Binding}" ItemTemplateSelector="{StaticResource MyGridTemplateSelector}" MinItemWidth="300" MaxItemWidth="600" ScrollViewer.VerticalScrollBarVisibility="Hidden">
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<VariableSizedWrapGrid ItemHeight="180" Orientation="Horizontal"/>
</ItemsPanelTemplate>
</GridView.ItemsPanel>
</local:MyGridView>
</Border>
</ScrollViewer>
</Grid>
</Border>
</Grid>
</Page>
using System.Linq;
using Windows.UI;
using Windows.UI.Xaml.Controls;
using System.Reflection;
namespace App1
{
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent( );
// just some sample data
var colors = typeof( Colors )
.GetRuntimeProperties( )
.Take( 140 )
.Select( ( x, index ) => new
{
Color = (Color) x.GetValue( null ),
Name = x.Name,
Index = index,
} );
this.DataContext = colors;
}
}
}
If you will ever think "I know that from somewhere" you should have a look at Jerry Nixon's blog :o)
According to MSDN the ItemWidth can be set to Auto.
The default value of ItemHeight and ItemWidth is not 0, it is Double.NaN. ItemHeight and ItemWidth support the ability to be an unset "Auto" value. Because ItemHeight and ItemWidth are Double values, Double.NaN is used as a special value to represent this "Auto" behavior. The layout system interprets the "Auto" value to generally mean that the object should be sized to the available size in layout, instead of to a specific pixel value.
I don't know if this will result in the behavior you want though. If it doesn't then you might be able to get it by binding the ItemWidth to a property where you calculate the item width based on the width of the grid. It would look something like this:
float DynamicItemWidth {
get {
int ItemMinimumWidth = 300, margin = 16; //just some guesses
var gridWidth = ...;
var numberOfColumns = gridWidth % ItemMinimumWidth;
var itemWidth = (gridWidth - margin * (numberOfColumns - 1)) / numberOfColumns;
return itemWidth;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With