如何使用网格在 ItemsControl 中动态布置列表项?

How can I dynamically lay out list items within an ItemsControl using a Grid?

我有一个字符串列表,足以代表我需要布置的内容。

我也到处读到,为了完成我需要做的事情,我最好的选择是 ItemsControl

问题是 ItemsControl 与开箱即用的 Grid 不兼容。

幸运的是,我发现 a few articles 的代码帮助我指明了正确的方向。

不幸的是,这不是我需要的一切。

这是我正在使用的 DataContext(我很确定我的问题在于我实现它的方式):

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

这是 GridItemsControl 的代码,我从链接文章中找到的相关代码,呃,Frakensteined 一起:

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;
    }
}

这是 window 的 XAML,我一直在其中测试此控件:

<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>

(看起来像这样)

这是我用来绑定内容行和列值失败的 MathConverter 的代码:

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

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

这就是问题所在:

第一个当然什么也没有显示,因为ListViewModel的Current值默认为null。说得通。但是在第二个中,单击“下一步”后,所有元素都堆积在一起,这就是问题所在。

我对问题是什么有一些想法:

AlternationCount="{Binding Count}"

Count 不是 IEnumerable 的 属性(Current 是什么)。 我也试过 AlternationCount="{Binding Path=(Count( ))}" 但结果相同。

我很确定问题出在我实现 Viewmodel 的方式和我如何绑定以获得 AlternationCount 以便我可以在网格中布置元素之间的某个地方......但到目前为止因为我可以接受这个 - 除了将 Current 更改为 List(我尝试过但也没有用),我没有想法。

我在这里做错了什么?如何让 ItemsControl 智能地布置列表的内容?

我修改了 viewModel 以简化视图绑定

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

和 ItemsControl 标记

<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>

看来问题是为每个项目计算 Grid.RowGrid.Column