How can I dynamically lay out list items within an

2019-09-03 04:11发布

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)

enter image description here

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 ]( );
    }
}

And this is the problem : enter image description here enter image description here

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?

1条回答
闹够了就滚
2楼-- · 2019-09-03 04:37

I modified viewModel to simplify view bindings

private IEnumerable<string> _Current;
public IEnumerable<string> Current
{
    get { return this._Current; }
    set
    {
        this._Current = value;
        _currentCount = _Current.Count();
        this.OnPropertyChanged("Current");
        this.OnPropertyChanged("CurrentCount");
        this.OnPropertyChanged("CurrentItems");
    }
}

private int _currentCount;
public int CurrentCount
{
    get { return _currentCount; }            
}

// or create a class instead of anonymous objects
public IEnumerable<object> CurrentItems
{
    get { return Current.Select((item, idx) => new { Item = item, Row = idx / 2, Column = idx % 2 }); }
}

and ItemsControl markup

<Components:GridItemsControl
    ItemsSource="{Binding CurrentItems}"
    AlternationCount="{Binding CurrentCount}"
    Grid.ColumnSpan="7">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Grid
                Components:GridItemsControl.RowCount="{
                    Binding Path=CurrentCount,
                    Converter={StaticResource ResourceKey=Math}, 
                    ConverterParameter=/2}"
                Components:GridItemsControl.ColumnCount="2"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <TextBlock
                Grid.Row="{Binding Path = Row}"
                Grid.Column="{Binding Path = Column}"
                Text="{Binding Item}"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</Components:GridItemsControl>

it seems the problem was calculating Grid.Row and Grid.Column for each item.

enter image description here

查看更多
登录 后发表回答