I have a list of a list of strings which is an adequate stand in for what I need to lay out.
I've also read everywhere that in order to accomplish what I need to do, my best bet is an ItemsControl
.
The problem is that ItemsControl
doesn't play nice with Grid
out of the box.
Fortunately I've found a few articles with code that has helped get me pointed in the right direction.
Unfortunately, it's not everything I need.
This is the DataContext with which I am working ( and I am pretty certain that my problem lies therein with the manner in which I have implemented it ) :
public class ListViewModel : INotifyPropertyChanged {
private IEnumerable<IEnumerable<string>> _Items;
public ReadOnlyCollection<IEnumerable<string>> Items {
get { return this._Items.ToList( ).AsReadOnly( ); }
}
private IEnumerable<string> _Current;
public IEnumerable<string> Current {
get { return this._Current; }
set {
this._Current = value;
this.OnPropertyChanged( "Current" );//.DontBlock( ).Wait( );
}
}
public ListViewModel( ) {
this._Items = new List<IEnumerable<string>>( );
List<string> stringsList;
for ( int x = 0; x < 10; x++ ) {
stringsList = new List<string>( );
for ( int y = x * 4; y < 4 + ( x * 4 ); y++ )
stringsList.Add( y.ToString( ) );
( this._Items as List<IEnumerable<string>> ).Add( stringsList );
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged( string p ) {
if ( this.PropertyChanged != null )
this.PropertyChanged( this, new PropertyChangedEventArgs( p ) );
}
public void First( ) { this.Current = this._Items.First( ); }
public void Previous( ) {
int i = this.Items.IndexOf( this.Current );
if ( i <= 0 )
this.Current = this.Items.Last( );
else
this.Current = this.Items[ i - 1 ];
}
public void Next( ) {
int i = this.Items.IndexOf( this.Current );
if ( i + 1 >= this.Items.Count || i < 0 )
this.Current = this.Items.First( );
else
this.Current = this.Items[ i + 1 ];
}
public void Last( ) {
this.Current = this.Items.Last( );
}
}
This is the code for the GridItemsControl that I have, er, Frakensteined together from the relevant code that I have found within the linked articles :
public class GridItemsControl : ItemsControl {
#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(GridItemsControl),
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 = new GridLength( 1, GridUnitType.Star )
} );
}
#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(GridItemsControl),
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 = new GridLength( 1, GridUnitType.Star )
} );
}
#endregion
protected override DependencyObject GetContainerForItemOverride( ) {
ContentPresenter container =
(ContentPresenter) base.GetContainerForItemOverride();
if ( ItemTemplate == null ) {
return container;
}
FrameworkElement
content = (FrameworkElement)ItemTemplate.LoadContent();
BindingExpression
rowBinding = content.GetBindingExpression(Grid.RowProperty),
columnBinding = content.GetBindingExpression(Grid.ColumnProperty);
if ( rowBinding != null ) {
container.SetBinding( Grid.RowProperty, rowBinding.ParentBinding );
}
if ( columnBinding != null ) {
container.SetBinding( Grid.ColumnProperty, columnBinding.ParentBinding );
}
return container;
}
}
This is the XAML for the window in which I have been testing this control :
<Window
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:QMPQuestionTester"
xmlns:Controls="clr-namespace:TriviaEngine.Controls;assembly=TriviaEngine"
xmlns:Components="clr-namespace:WPFTools.Components;assembly=WPFTools"
xmlns:Converters="clr-namespace:WPFTools.Classes.Converters;assembly=WPFTools"
x:Class="QMPQuestionTester.MainWindow"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:ListViewModel/>
</Window.DataContext>
<Window.Resources>
<Converters:MathConverter x:Key="Math"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="5"/>
<RowDefinition Height="50"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="5"/>
<ColumnDefinition/>
<ColumnDefinition Width="5"/>
<ColumnDefinition/>
<ColumnDefinition Width="5"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button
x:Name="btnFirst" Content="First" Grid.Row="2" Click="Nav"/>
<Button
x:Name="btnPrev" Content="Prev." Grid.Row="2" Grid.Column="2" Click="Nav"/>
<Button
x:Name="btnNext" Content="Next" Grid.Row="2" Grid.Column="4" Click="Nav"/>
<Button
x:Name="btnLast" Content="Last" Grid.Row="2" Grid.Column="6" Click="Nav"/>
<Components:GridItemsControl
DataContext="{Binding Current}"
ItemsSource="{Binding}"
AlternationCount="{Binding Count}"
Grid.ColumnSpan="7">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid
Components:GridItemsControl.RowCount="{
Binding Count,
Converter={StaticResource ResourceKey=Math},
ConverterParameter=/2}"
Components:GridItemsControl.ColumnCount="2"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock
Grid.Row="{Binding
Path = (ItemsControl.AlternationIndex),
RelativeSource={RelativeSource TemplatedParent},
Converter={StaticResource Math},
ConverterParameter=/2}"
Grid.Column="{Binding
Path = (ItemsControl.AlternationIndex),
RelativeSource={RelativeSource TemplatedParent},
Converter={StaticResource Math},
ConverterParameter=%2}"
Text="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</Components:GridItemsControl>
</Grid>
</Window>
(which looks like this)
This is the code for the MathConverter that I am using to fail at binding the contents row and column values :
public class MathConverter : IValueConverter {
public object Convert( object value, Type targetType, object parameter, CultureInfo culture ) {
if ( parameter == null )
return value;
double
Foo,
Bar = ( double )System.Convert.ChangeType( value, typeof( double ) );
switch ( ( ( string )( parameter ) ).ToCharArray( )[ 0 ] ) {
case '%':
Foo = Bar % double.Parse(
( ( string )( parameter ) ).TrimStart( new char[ ] { '%' } ) );
break;
case '*':
Foo = Bar * double.Parse(
( ( string )( parameter ) ).TrimStart( new char[ ] { '*' } ) );
break;
case '/':
Foo = Bar / double.Parse(
( ( string )( parameter ) ).TrimStart( new char[ ] { '/' } ) );
break;
case '+':
Foo = Bar + double.Parse(
( ( string )( parameter ) ).TrimStart( new char[ ] { '+' } ) );
break;
case '-':
if ( ( ( string )( parameter ) ).Length > 1 ) {
Foo = Bar - double.Parse(
( ( string )( parameter ) ).TrimStart( new char[ ] { '-' } ) );
} else Foo = Bar * -1.0D;
break;
default:
return DependencyProperty.UnsetValue;
}
return System.Convert.ChangeType( Foo, targetType );
}
And this is the rest of the code for the Window :
public partial class MainWindow : Window {
private Dictionary<Button, Action> NavCMDs;
public MainWindow( ) {
InitializeComponent( );
this.NavCMDs = new Dictionary<Button, Action>( ) {
{ this.btnFirst, ( ) => ( this.DataContext as ListViewModel ).First( ) },
{ this.btnPrev, ( ) => ( this.DataContext as ListViewModel ).Previous( ) },
{ this.btnNext, ( ) => ( this.DataContext as ListViewModel ).Next( ) },
{ this.btnLast, ( ) => ( this.DataContext as ListViewModel ).Last( ) },
};
}
private void Nav( object sender, RoutedEventArgs e ) {
this.NavCMDs[ sender as Button ]( );
}
}
In the first, of course, nothing is displayed because the ListViewModel Current value is null by default. Makes sense. But in the second, after clicking Next, all the elements get piled on top of each other, and that's the problem.
I have an inkling of an idea as to what the problem is :
AlternationCount="{Binding Count}"
Count
is not a property of an IEnumerable
( which is what Current is ).
I've also tried AlternationCount="{Binding Path=(Count( ))}"
but that yielded the same results.
I'm pretty certain the problem lies somewhere between the way that I have implemented the Viewmodel and how I am binding to get the AlternationCount so that I can lay out the elements in the grid... but that's as far as I can take this - Besides changing Current to a List ( which I tried and that did not work either ), I'm out of ideas.
What am I doing wrong here? How can I make the ItemsControl intelligently lay out the contents of the list?
I modified viewModel to simplify view bindings
and ItemsControl markup
it seems the problem was calculating
Grid.Row
andGrid.Column
for each item.