Есть ListBox в котором может быть несколько сотен элементов. Обычный скролл в данном случае не очень удобен, поэтому хочется сделать возможность листать содержимое страницами. Соответственно под ListBox'ом добавить панель навигации: < Назад 1 2 ... 15 Вперед > Гугл по вопросу постраничного скролла ListBox'а подсказывает использовать VerticalOffset и ViweportHeight. Но это подразумевает много кода в codebehind. Подскажите, можно ли реализовать такое, используя MVVM? И если можно, то как?
-
1Это смотрели: https://ru.stackoverflow.com/a/616413/218063 ? – Андрей NOP May 26 '20 at 16:08
1 Answers
Я много где поискал, но не нашел вменяемого примера для MVVM кроме этого и решил все-таки написать еще один, и сделать его как можно проще. На помощь приходит ICollectionView.
Думаю, не надо объяснять, что такое MVVM, поэтому для полноты картины я опубликую здесь вспомогательные классы, которые я использовал в решении.
// реализация INPC для наследования во ViewModel
public class NotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
// реализация ICommand для удобного использования команд
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter);
public void Execute(object parameter) => _execute(parameter);
}
Решение создает демо-коллекцию и позволяет
- Менять количество элементов на страницу
- Переключать страницы
Полная разметка
<Window x:Class="WpfApp2.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:WpfApp2"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<local:PagerConverter x:Key="PagerConverter"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding ItemsPerPage}" Width="40" Margin="5"/>
<TextBlock Text="Page:" Margin="5,5,0,5"/>
<TextBlock Text="{Binding Page}" Margin="5" FontWeight="Bold"/>
</StackPanel>
<ListBox Margin="5" Grid.Row="1" ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Margin="5,0" Text="{Binding Index}"/>
<TextBlock Margin="5,0" Grid.Column="1" Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ItemsControl Grid.Row="2" Margin="3,5">
<ItemsControl.ItemsSource>
<MultiBinding Converter="{StaticResource PagerConverter}">
<Binding Path="Items.Count"/>
<Binding Path="ItemsPerPage"/>
</MultiBinding>
</ItemsControl.ItemsSource>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding}"
Command="{Binding DataContext.SetPageCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"
Margin="2" Padding="5,0"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
Класс для хранения данных
public class Item
{
public int Index { get; set; }
public string Name { get; set; }
}
ViewModel
public class MainViewModel : NotifyPropertyChanged
{
private int _itemsPerPage;
private ObservableCollection<Item> _items;
private int _page;
private ICommand _setPageCommand;
private ICollectionView Collection => CollectionViewSource.GetDefaultView(Items);
public ObservableCollection<Item> Items
{
get => _items;
set
{
_items = value;
OnPropertyChanged();
}
}
public int Page
{
get => _page;
set
{
_page = value;
OnPropertyChanged();
Collection.Refresh(); // сообщает ICollectionView, что надо перефильтроваться
}
}
public int ItemsPerPage
{
get => _itemsPerPage;
set
{
_itemsPerPage = value;
OnPropertyChanged();
Collection.Refresh();
}
}
public ICommand SetPageCommand => _setPageCommand ?? (_setPageCommand = new RelayCommand(parameter =>
{
if (parameter is int page)
{
Page = page;
}
}));
public MainViewModel()
{
Items = new ObservableCollection<Item>();
for (int i = 0; i < 250; i++)
{
Items.Add(new Item { Index = i, Name = $"Element {i}" });
}
ItemsPerPage = 15;
Page = 1;
Collection.Filter = item => {
int index = Items.IndexOf(item as Item);
return index >= (Page - 1) * ItemsPerPage && index < Page * ItemsPerPage;
};
}
}
Конвертер для генерации кнопок пейджера
public class PagerConverter : IMultiValueConverter
{
// генератор списка с числами по порядку
private IEnumerable<int> PagesGenerator()
{
int i = 1;
while (true)
{
yield return i++;
}
}
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values[0] is int count && values[1] is int itemsPerPage)
{
int pages = count / itemsPerPage + 1;
return new List<int>(PagesGenerator().Take(pages));
}
return null;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => null;
}
Выглядит это так
Вот и всё, пробуйте, экспериментируйте. Я не сделал кнопки Назад и Вперед, но вы можете вставить их в горизонтальную StackPanel слева и справа от ItemsControl и привязать их к командам, которые делают Page++ и Page--, или к одной команде с параметром.
На самом деле, я впервые реализовал этот механизм, хоть и много про него слышал, поэтому много рассказать про то, как это работает, не могу. Оно просто работает. :) В коде так же могут быть недочеты, готов их исправить, оставляйте комментарии.
- 49,560
-
Спасибо за пример, очень наглядно! Но главный вопрос всё равно остаётся открытым: я не знаю изначально сколько элементов влезает в ListBox. Размер окна программы ведь может измениться, а также могут быть разные разрешения экрана. Соответственно ItemsPerPage может меняться во время работы программы и зависит от размера области отображения ListBox. Отловить во ViewModel изменение размера LixtBox я могу, прибиндив команду на SizeChanged. Но как мне вытащить VerticalOffset и ViweportHeight из вложенного в listbox ScrollViewer'а и передать/забиндить их на свойства вьюмодели? – Alex_Lux May 27 '20 at 08:07
-
@Alex_Lux а высоту каждого элемента списка мы знаем, или она тоже динамическая? – aepot May 27 '20 at 08:38
-
-
@Alex_Lux а вы уверены, что это точно нужно? Придется писать обработчики событий, и высчитывать количество элементов для отображения относительно
ListBox.ActualHeightи передавать это воViewModel, получится не очень сложная, то слегка костыльная реализация. Почему бы просто не совместить достоинства прокруткиScrollView, которые нативно поддерживаются вListBoxи постраничного просмотра, который я уже реализовал? К тому же можно где-нибудь в настройках позволить юзеру менять количество элементов, отображаемых на странице, и проблема решена. – aepot May 27 '20 at 10:15 -
@Alex_Lux единственный вменяемый способ реализации, который я вижу при условии не нарушения MVVM и соблюдения SRP - это
UserControl, унаследованный отListBox, в котором вместо прокрутки будет высчитываться и ограничиваться количество видимых элементов. Придется в него и постраничную фичу уносить и все параметры постраничного просмотра вDepencyProperty. Получится своеобразныйPagedListBox. Решение весьма комплексное, и я не смогу его быстро реализовать. – aepot May 27 '20 at 10:19
