wykład 7 – konwersja, walidacja, szablony, widoki
<Window ... Title="Księgarnia">
<Grid>
...
<ListBox Name="lista" DisplayMemberPath="Title"/>
<GridSplitter Grid.Column="1" Width="5"
HorizontalAlignment="Center"/>
<Grid Grid.Column="2" DataContext="{Binding
ElementName=lista, Path=SelectedItem}" >
...
<Label ...>Tytuł:</Label>
<TextBox ... Text="{Binding Path=Title}" />
<Label ...>Autor:</Label>
<TextBox ... Text="{Binding Path=Author}"/>
<Label ...>Cena:</Label>
<TextBox ... Text="{Binding Path=Price}"/>
</Grid>
</Grid>
</Window>
public string Title { get; set; } public string Author { get; set; } public decimal Price { get; set; }
public Book(string title, string author, decimal price) {
Title = title;
Author = author;
Price = price;
} }
List<Book> lst = new List<Book>();
lst.Add(new Book("Lód", "Jacek Dukaj", 57.99M));
lst.Add(new Book("Inne pieśni", "Jacek Dukaj", 48.50M));
...
lista.ItemsSource = lst;
• Odpowiada za konwertowanie źródłowych danych, zanim zostaną wyświetlone (np.
z niskopoziomowej reprezentacji w postać czytelną dla użytkownika) oraz konwersję nowych wartości, nim zostaną zapamiętane.
• Używana jest do:
◦ formatowania danych (np. konwersja liczby na string),
◦ tworzenia obiektów WPF (np. przy wyświetlaniu obrazków),
◦ warunkowej modyfikacji pewnych własności elementów interfejsu.
[ValueConversion(typeof(decimal), typeof(string))]
public class PriceConverter : IValueConverter {
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture) {
decimal price = (decimal)value;
return price.ToString("C", culture);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
string price = value.ToString();
decimal result;
if (Decimal.TryParse(price, NumberStyles.Any, culture, out result)) { return result;
}
return value;
} }
• Ustawienia języka:
<Window ...
xmlns:local="clr-namespace:WpfApp1"
Language="pl-PL">
• Wybór konwertera:
<Label Grid.Row="2" Margin="3">Cena:</Label>
<TextBox Grid.Column="1" Grid.Row="2" Margin="3">
<TextBox.Text>
<Binding Path="Price">
<Binding.Converter>
<local:PriceConverter/>
</Binding.Converter>
</Binding>
</TextBox.Text>
</TextBox>
• Konwerter w zasobach:
<Window.Resources>
<local:PriceConverter x:Key="PriceConverter" />
</Window.Resources>
• Korzystanie:
<TextBox Grid.Column="1" Grid.Row="2" Margin="3"
Text="{Binding Path=Price, Converter={StaticResource PriceConverter}}"/>
• Baza danych może przechowywać dane binarne reprezentujące obraz produktu.
• Konwerter pozwala skonwertować tablicę bajtów na obiekt klasy BitmapImage:
◦ tworzymy obiekt BitmapImage,
◦ odczytujemy dane obrazka w MemoryStream,
◦ wywołujemy BitmapImage.BeginInit(),
◦ ustawiamy własność StreamSource na nasz MemoryStream,
◦ wywołujemy EndInit() aby zakończyć ładowanie obrazka.
• Prostszy przykład: pole ImagePath przechowuje ścieżkę, a obrazki są zapisane na dysku.
public class ImagePathConverter : IValueConverter {
private string imageDirectory =
Directory.GetCurrentDirectory();
public string ImageDirectory { get { return imageDirectory; } set { imageDirectory = value; } }
public object Convert(...) { string imagePath =
System.IO.Path.Combine(ImageDirectory, (string)value);
return new BitmapImage(new Uri(imagePath));
}
public object ConvertBack(...) {
throw new NotSupportedException(); } }
• obrazek można odczytać też ze zdalnej lokacji:
return new BitmapImage(new Uri(
(string)value, UriKind.Absolute));
• Wykorzystanie:
<Window.Resources>
<local:ImagePathConverter x:Key="ImagePathConverter" />
</Window.Resources>
<Image Margin="3" Grid.Row="3" Grid.Column="1"
Stretch="Uniform" HorizontalAlignment="Center"
Source="{Binding Path=ImagePath, Converter={StaticResource ImagePathConverter}}">
• W wypadku braku obrazka – możemy łapać wyjątek w metodzie Convert() i np.
zwracać Binding.DoNothing lub jakiś obrazek domyślny.
public class PriceToBackgroundConverter : IValueConverter {
public decimal MaximumPriceToHighlight { get; set; } public Brush HighlightBrush { get; set; }
public Brush DefaultBrush { get; set; } public object Convert(...)
{
decimal price = (decimal)value;
if (price <= MaximumPriceToHighlight) return HighlightBrush;
else
return DefaultBrush;
}
public object ConvertBack(...) {
throw new NotSupportedException();
} }
<Window.Resources>
...
<local:PriceToBackgroundConverter x:Key="PriceToBackgroundConverter"
DefaultBrush="{x:Null}" HighlightBrush="GreenYellow"
MaximumPriceToHighlight="29.99"/>
</Window.Resources>
<Grid DataContext="{Binding ElementName=lista,
Path=SelectedItem}" Grid.Column="2" Background="{Binding Path=Price, Converter={StaticResource
PriceToBackgroundConverter}}">
...
</Grid>
• Pozwala kilka własności skonwertować na jedną wartość.
<Window.Resources>
<local:PriceVatConverter x:Key="PriceVatConverter" />
</Window.Resources>
<TextBox Grid.Column="1" Grid.Row="2" Margin="3">
<TextBox.Text>
<MultiBinding Converter="{StaticResource
PriceVatConverter}">
<Binding Path="Price"></Binding>
<Binding Path="VAT"></Binding>
</MultiBinding>
</TextBox.Text>
</TextBox>
• Wartości w tablicy values są w tej samej kolejności, co Bindingi w definicji w XAMLu.
public class PriceVatConverter : IMultiValueConverter {
public object Convert(object[] values, ...) {
try {
decimal price = (decimal)values[0];
decimal vat = (decimal)values[1];
return (price * (1 + vat)).ToString("C",
culture);
} catch {return Binding.DoNothing;}
}
public object[] ConvertBack(object value, Type[] t, ...) {
throw new NotSupportedException();
} }
• Pozwala kontrolować poprawność danych przy przesyłaniu ich z elementu docelowego do źródła.
Rzucanie wyjątku:
private decimal price;
public decimal Price {
get { return price; } set
{
if (value <= 0)
throw new ArgumentException(
"Cena musi być większa od 0.");
price = value;
} }
• Wyjątki wiązania danych są ignorowane, dlatego potrzebujemy jeszcze reguły walidacji:
<TextBox Grid.Column="1" Grid.Row="2" Margin="3">
<TextBox.Text>
<Binding Path="Price">
<Binding.Converter>
<StaticResource
ResourceKey="PriceConverter"/>
</Binding.Converter>
<Binding.ValidationRules>
<ExceptionValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
• W wypadku nieudanej walidacji WPF:
◦ ustawia własność dołączoną Validation.HasError na true,
◦ tworzy ValidationError zawierający szczegóły błędu,
◦ jeśli ustawiono Binding.NotifyOnValidationError na true, podnosi zdarzenie Validation.Error.
• Zmienia się również wygląd kontrolki (wykorzystanie szablonu Validation.ErrorTemplate).
• Niekiedy nie chcemy rzucać wyjątków przy każdym błędzie użytkownika:
public class Book : IDataErrorInfo {
...
private decimal price;
public decimal Price {
get { return price; } set { price = value; } }
public class Book : IDataErrorInfo {
...
public string this[string columnName]
{
get {
if (columnName == "Price") {
if (price <= 0)
return "Cena musi być większa od 0.";
}
return null;
} }
public string Error { get { return null; } } }
• Inny przykład:
public string this[string columnName] { get {
if (propertyName == "Code") {
bool valid = true;
foreach (char c in Code) {
if (!Char.IsLetterOrDigit(c)) { valid = false;
break;
} }
if (!valid)
return "Może zawierać tylko cyfry i litery.";
}
return null;
} }
<TextBox Grid.Column="1" Grid.Row="2" Margin="3">
<TextBox.Text>
<Binding Path="Price">
<Binding.Converter>
<StaticResource
ResourceKey="PriceConverter"/>
</Binding.Converter>
<Binding.ValidationRules>
<DataErrorValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
• Możliwe jest łączenie obu podejść.
• Możemy skorzystać ze skrótu – zamiast dodawać ExceptionValidationRule i DataErrorValidationRule, ustawiamy na true:
◦ Binding.ValidatesOnExceptions
◦ Binding.ValidatesOnDataErrors
• Własne reguły walidacji.
public class PositivePriceRule : ValidationRule {
private decimal min = 0;
private decimal max = Decimal.MaxValue;
public decimal Min {
get { return min; } set { min = value; } }
public decimal Max {
get { return max; } set { max = value; } }
public class PositivePriceRule : ValidationRule {
...
public override ValidationResult Validate(object value, CultureInfo culture) {
decimal price = 0;
try {
if (((string)value).Length > 0)
price = Decimal.Parse((string)value,
NumberStyles.Any, culture);
} catch {
return new ValidationResult(false,
"Illegal characters.");
}
...
if ((price < Min) || (price > Max)) {
return new ValidationResult(false,
"Not in the range " + Min + " to " + Max + ".");
} else {
return new ValidationResult(true, null);
} } }
<TextBox Grid.Column="1" Grid.Row="2" Margin="3">
<TextBox.Text>
<Binding Path="Price">
<Binding.Converter>
<StaticResource
ResourceKey="PriceConverter"/>
</Binding.Converter>
<Binding.ValidationRules>
<local:PositivePriceRule Min="0.01"
Max="999.99" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
• Uwaga: możemy dodać dowolną liczbę reguł walidacji.
• Reakcja na błędy walidacji:
◦ flaga NotifyOnValidationError:
<Binding Path="Price" NotifyOnValidationError="True">
...
</Binding>
◦ zdarzenie:
<Grid Validation.Error="validationError">
◦ obsługa:
private void validationError(object sender, ...) {
if (e.Action == ValidationErrorEventAction.Added) {
MessageBox.Show(e.Error.ErrorContent.ToString());
} }
• Lista błędów walidacji:
private void cmdOK_Click(object sender, RoutedEventArgs e) {
string message;
if (FormHasErrors(out message)) {
// Errors still exist.
MessageBox.Show(message);
} else {
// ...
} }
private bool FormHasErrors(out string message) {
StringBuilder sb = new StringBuilder();
GetErrors(sb, gridProductDetails);
message = sb.ToString();
return message != "";
}
private void GetErrors(StringBuilder sb, DependencyObject obj) {
foreach (object child in
LogicalTreeHelper.GetChildren(obj)) {
TextBox element = child as TextBox;
if (element == null) continue;
if (Validation.GetHasError(element)) {
sb.Append(element.Text + " has errors:\r\n");
foreach (ValidationError error in
Validation.GetErrors(element)) { sb.Append(" " +
error.ErrorContent.ToString());
sb.Append("\r\n");
} }
// sprawdź dzieci
GetErrors(sb, element);
} }
• Własne style powiadomienia:
<TextBox Grid.Column="1" Grid.Row="2" Margin="3,3,20,3">
<Validation.ErrorTemplate>
<ControlTemplate>
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Right"
Foreground="Red" FontSize="14"
FontWeight="Bold">*</TextBlock>
<Border BorderBrush="Green"
BorderThickness="1">
<AdornedElementPlaceholder />
</Border>
</DockPanel>
</ControlTemplate>
</Validation.ErrorTemplate>
<TextBox.Text>
...
</TextBox.Text>
</TextBox>
<TextBlock ... ToolTip="{Binding
ElementName=adornerPlaceholder,
Path=AdornedElement.(Validation.Errors)[0]
.ErrorContent}">*</TextBlock>
...
<AdornedElementPlaceholder Name="adornerPlaceholder" />
• Fragment kodu XAMLa, który mówi w jaki sposób ma być wyświetlany dowiązany obiekt danych:
◦ kontrolki zawartości obsługują to poprzez własność ContentTemplate
◦ kontrolki list – poprzez ItemTemplate (stosowane do każdego obiektu kolekcji) Pozwala zastąpić to:
<ListBox Name="lista" Margin="5" DisplayMemberPath="Title"/>
Tym:
<ListBox Name="lista" Margin="5">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Title}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ListBox Name="lista" Margin="5"
HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Margin="5" BorderThickness="1"
BorderBrush="SteelBlue" CornerRadius="4">
<Grid Margin="3">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock FontWeight="Bold"
Text="{Binding Path=Title}"></TextBlock>
<TextBlock Grid.Row="1" Text="{Binding Path=Author}"></TextBlock>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
• Umieszczanie szablonów w zasobach:
<Window.Resources>
...
<DataTemplate x:Key="BookDataTemplate">
<Border Margin="5" BorderThickness="1"
BorderBrush="SteelBlue" CornerRadius="4">
<Grid Margin="3">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock FontWeight="Bold" Text="{Binding Path=Title}"></TextBlock>
<TextBlock Grid.Row="1" Text="{Binding Path=Author}"></TextBlock>
</Grid>
</Border>
</DataTemplate>
</Window.Resources>
• Korzystanie z szablonów umieszczonych w zasobach:
<ListBox Name="lista" Margin="5"
HorizontalContentAlignment="Stretch"
ItemTemplate="{StaticResource BookDataTemplate}"/>
<DataTemplate x:Key="BookDataTemplate">
<Border ...>
<Grid Margin="3">
<Grid.RowDefinitions>
<RowDefinition/><RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"
SharedSizeGroup="ikona"></ColumnDefinition>
<ColumnDefinition ></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="1" FontWeight="Bold"
Text="{Binding Path=Title}"></TextBlock>
<TextBlock Grid.Column="1" Grid.Row="1"
Text="{Binding Path=Author}"></TextBlock>
<Image Grid.RowSpan="2" MaxHeight="64"
Source="{Binding Path=ImagePath,
Converter={StaticResource ImagePathConverter}}">
</Image>
</Grid>
</Border> </DataTemplate>
<Grid Name="gridProductDetails"
Grid.IsSharedSizeScope="True">
<ListBox Name="lista" Margin="5"
HorizontalContentAlignment="Stretch"
ItemTemplate="{StaticResource BookDataTemplate}"/>
<DataTemplate x:Key="BookDataTemplate">
...
<Button Click="cmdDoKoszyka" Tag="{Binding Path=ProductID}">Do koszyka...</Button>
</DataTemplate>
private void cmdDoKoszyka(object sender, RoutedEventArgs e) {
Button cmd = (Button)sender;
int productID = (int)cmd.Tag;
//...
}
Inne rozwiązanie:
<DataTemplate x:Key="BookDataTemplate">
...
<Button Click="cmdDoKoszyka" Tag="{Binding}">
Do koszyka...</Button>
</DataTemplate>
private void cmdDoKoszyka(object sender, RoutedEventArgs e) {
Button cmd = (Button)sender;
Book book = (Book)cmd.Tag;
lista.SelectedItem = book;
//...
}
• Różnicowanie szablonów danych:
<DataTemplate x:Key="BookDataTemplate">
<Border ... Background="{Binding Path=Price,
Converter={StaticResource PriceToBackgroundConverter}}">
...
</Border>
</DataTemplate>
• Wybór szablonów:
public class BookTemplateSelector : DataTemplateSelector {
public override DataTemplate SelectTemplate(object item, DependencyObject container) {
Book product = (Book)item;
Window window = Application.Current.MainWindow;
if (product.CategoryName == "Horror") {
return
(DataTemplate)window.FindResource("HorrorBookTemplate");
} else {
return
(DataTemplate)window.FindResource("DefaultBookTemplate");
} } }
<Window.Resources>
<DataTemplate x:Key="DefaultBookTemplate">
...
</DataTemplate>
<DataTemplate x:Key="HorrorBookTemplate">
<Border Margin="5" BorderThickness="2"
BorderBrush="Red" CornerRadius="4"
Background="Black" TextBlock.Foreground="White">
...
</Border>
</DataTemplate>
</Window.Resources>
<ListBox Name="lista" Margin="5"
HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplateSelector>
<local:BookTemplateSelector/>
</ListBox.ItemTemplateSelector>
</ListBox>
• Lepsze (bardziej uniwersalne) rozwiązanie:
public class SingleCriteriaHighlightTemplateSelector : DataTemplateSelector
{
public DataTemplate DefaultTemplate { get; set; } public DataTemplate HighlightTemplate { get; set; } public string PropertyToEvaluate { get; set; }
public string PropertyValueToHighlight { get; set; } public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
Product product = (Product)item;
Type type = product.GetType();
PropertyInfo property =
type.GetProperty(PropertyToEvaluate);
{
return HighlightTemplate;
} else {
return DefaultTemplate;
} } }
<ListBox Name="lista" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplateSelector>
<local:SingleCriteriaHighlightTemplateSelector DefaultTemplate="{StaticResource DefaultBookTemplate}"
HighlightTemplate="{StaticResource HorrorBookTemplate}"
PropertyToEvaluate="CategoryName"
PropertyValueToHighlight="Horror"
>
</local:SingleCriteriaHighlightTemplateSelector>
</ListBox.ItemTemplateSelector>
</ListBox>">
• Uwaga: wybór szablonu następuje raz, w momencie tworzenia dowiązania. Jeśli zmiana stanu obiektu może wymagać wyboru innego szablonu, możemy wymusić to ręcznie (np. w PropertyChanged):
DataTemplateSelector selector = lista.ItemTemplateSelector;
lista.ItemTemplateSelector = null;
lista.ItemTemplateSelector = selector;
• Możemy zastąpić domyślny kontener listy:
<ListBox Name="lista" Margin="5"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplateSelector>
<local:BookTemplateSelector/>
</ListBox.ItemTemplateSelector>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel></WrapPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
• Widok – znajduje się pomiędzy źródłem danych a powiązaną kontrolką.
• To widok śledzi „aktualny element listy”, udostępnia sortowanie, filtrowanie, grupowanie.
• Widok jest typu:
◦ BindingListCollectionView – jeśli źródło danych jest typu IbindingList,
◦ ListCollectionView – jeśli źródło nie jest typu IbindingList, ale Ilist
◦ CollectionView – jeśli nie jest ani IbindingList, ani Ilist, a tylko Ienumerable.
• Dostęp do widoku:
ICollectionView view =
CollectionViewSource.GetDefaultView(lista.ItemsSource);
• Pozwala pokazać jedynie podzbiór rekordów listy spełniających pewne warunki.
ListCollectionView view =
(ListCollectionView)CollectionViewSource.GetDefaultView(lista .ItemsSource);
view.Filter = FilterBook;
public bool FilterBook(Object item) {
Book product = (Book)item;
return (product.Price> 100);
} albo:
view.Filter = delegate(object item) {
Book product = (Book)item;
return (product.Price > 30);
};
public class ProductByPriceFilter {
public decimal MinimumPrice {
get;
set;
}
public ProductByPriceFilter(decimal minimumPrice) {
MinimumPrice = minimumPrice;
}
public bool FilterItem(Object item) {
Book product = item as Book;
if (product != null) {
return (product.Price > MinimumPrice);
}
return false;
} }
private void cmdFilter_Click(object sender, ...) {
decimal minimumPrice;
if (Decimal.TryParse(txtMinPrice.Text, out minimumPrice)) {
ListCollectionView view =
CollectionViewSource.GetDefaultView(lista.ItemsSource) as ListCollectionView;
if (view != null) {
ProductByPriceFilter filter =
new ProductByPriceFilter(minimumPrice);
view.Filter = filter.FilterItem;
} } }
Usunięcie filtra:
view.Filter = null;
• Uwaga: nie można łączyć kilku filtrów – należy raczej zaprojektować filtr z wieloma warunkami.
• Sortowanie na podstawie wskazanej własności danych:
ListCollectionView view =
(ListCollectionView)CollectionViewSource.GetDefaultView(lista .ItemsSource);
view.SortDescriptions.Add(new SortDescription("Title", ListSortDirection.Ascending));
• Własna procedura sortowaniea (tylko dla ListCollectionView).
public class SortByNameLength : System.Collections.IComparer {
public int Compare(object x, object y) {
Book bookX = (Book)x;
Book bookY = (Book)y;
return
bookX.Title.Length.CompareTo(bookY.Title.Length);
} }
view.CustomSort = new SortByNameLength();
• Jest zbliżone do sortowania:
view.GroupDescriptions.Add(new
PropertyGroupDescription("Author"));
• A na czym polega różnica? Czyli: jak rozróżnić grupy?
• ItemsControl.GroupStyle:
◦ ContainerStyle – styl dla każdego elementu grupy
◦ ContainerStyleSelector
◦ HeaderTemplate – nagłówek dla grupy
◦ HeaderTemplateSelector
◦ Panel – wybór panelu przechowującego grupę
<ListBox ...>
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}"
FontWeight="Bold" Foreground="White" Background="LightGreen"
Margin="0,5,0,0" Padding="3"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListBox.GroupStyle>
</ListBox>
• Uwaga: nie dowiązujemy do obiektu danych, ale do PropertyGroupDescription, stąd własność Name.
• Grupowanie przedziałami:
public class PriceRangeProductGrouper : IValueConverter {
public int GroupInterval {
get;
set;
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
decimal price = (decimal)value;
return String.Format(culture, "Mniej niż {0:C}", GroupInterval);
} else {
int interval = (int)price / GroupInterval;
int lowerLimit = interval * GroupInterval;
int upperLimit = (interval + 1) * GroupInterval;
return String.Format(culture, "{0:C} – {1:C}", lowerLimit, upperLimit);
} }
public object ConvertBack(...) {
throw new NotSupportedException(
"This converter is for grouping only.");
} }
view.SortDescriptions.Add(new SortDescription("Price", ListSortDirection.Ascending));
PriceRangeProductGrouper grouper = new PriceRangeProductGrouper();
grouper.GroupInterval = 10;
view.GroupDescriptions.Add(new
PropertyGroupDescription("Price", grouper));
• Widok udostępnia metody i własności służące do nawigacji, np. Count, CurrentItem, CurrentPosition, MoveCurrentToFirst(), MoveCurrentToLast(), MoveCurrentToNext(), MoveCurrentToPrevious(), MoveCurrentToPosition().
• Można to robić nawet bez listy:
<Window ...>
...
<Grid>
...
<Label ...>Tytuł:</Label>
<TextBox ...Text="{Binding Path=Title}" />
<Label ...>Autor:</Label>
<TextBox ...Text="{Binding Path=Author}"/>
...
<Button Name="cmdPrev" ...><</Button>
<TextBlock Name="lblPosition" .../>
<Button Name="cmdNext" ...>></Button>
</Grid>
</Window>
• W klasie okna zadeklarujmy referencję na widok:
private ListCollectionView view;
• W momencie ładowania okna stwórzmy lub załądujmy listę danych i pobierzmy widok:
List<Book> lst = new List<Book>();
lst.Add(...);
...
this.DataContext = lst;
view =
(ListCollectionView)CollectionViewSource.GetDefaultView(this.
DataContext);
view.CurrentChanged += view_CurrentChanged;
private void view_CurrentChanged(object sender, EventArgs e) {
lblPosition.Text = "Pozycja " +
(view.CurrentPosition+1).ToString() + " z " + view.Count.ToString();
cmdPrev.IsEnabled = view.CurrentPosition > 0;
cmdNext.IsEnabled = view.CurrentPosition < view.Count-1;
}
private void cmdPrev_Click(object sender, RoutedEventArgs e) {
view.MoveCurrentToPrevious();
}
private void cmdNext_Click(object sender, RoutedEventArgs e) {
view.MoveCurrentToNext();
}