PwT.N W7
Transkrypt
PwT.N W7
Budowa aplikacji w technologii .NET 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> 1/85 public class Book { 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; 2/85 3/85 Konwersja danych • • 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. 4/85 Value Converter [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; } } 5/85 Value Converter • 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> 6/85 Value Converter • 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}}"/> 7/85 Value Converter 8/85 Tworzenie obiektów z Value Converterem • • • 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. 9/85 Tworzenie obiektów z Value Converterem 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)); 10/85 Tworzenie obiektów z Value Converterem • 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. 11/85 Tworzenie obiektów z Value Converterem 12/85 Formatowanie warunkowe 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(); } } 13/85 Formatowanie warunkowe <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> 14/85 Formatowanie warunkowe 15/85 MultiConverter • 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> 16/85 MultiConverter • 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(); } } 17/85 MultiConverter 18/85 Walidacja • 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; } } 19/85 Walidacja • 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> 20/85 Walidacja 21/85 Walidacja • • 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). 22/85 Walidacja • 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; } } 23/85 Walidacja 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; } } } 24/85 Walidacja • 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; } } 25/85 Walidacja <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 26/85 Walidacja 27/85 Walidacja • 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; } } 28/85 Walidacja 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."); } 29/85 Walidacja ... if ((price < Min) || (price > Max)) { return new ValidationResult(false, "Not in the range " + Min + " to " + Max + "."); } else { return new ValidationResult(true, null); } } } 30/85 Walidacja <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. 31/85 Walidacja 32/85 Walidacja • 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()); } } 33/85 Walidacja 34/85 Walidacja • 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 { // ... } } 35/85 Walidacja private bool FormHasErrors(out string message) { StringBuilder sb = new StringBuilder(); GetErrors(sb, gridProductDetails); message = sb.ToString(); return message != ""; } 36/85 Walidacja 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); } } 37/85 Walidacja • 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> 38/85 Walidacja 39/85 Walidacja <TextBlock ... ToolTip="{Binding ElementName=adornerPlaceholder, Path=AdornedElement.(Validation.Errors)[0] .ErrorContent}">*</TextBlock> ... <AdornedElementPlaceholder Name="adornerPlaceholder" /> 40/85 Szablony danych • 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> 41/85 Szablony danych 42/85 Szablony danych <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> 43/85 Szablony danych 44/85 Szablony danych • 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> 45/85 Szablony danych • Korzystanie z szablonów umieszczonych w zasobach: <ListBox Name="lista" Margin="5" HorizontalContentAlignment="Stretch" ItemTemplate="{StaticResource BookDataTemplate}"/> 46/85 Szablony danych <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> 47/85 ... <Grid Name="gridProductDetails" Grid.IsSharedSizeScope="True"> <ListBox Name="lista" Margin="5" HorizontalContentAlignment="Stretch" ItemTemplate="{StaticResource BookDataTemplate}"/> 48/85 Szablony danych 49/85 Szablony danych <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; //... } 50/85 Szablony danych 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; //... } 51/85 Szablony danych 52/85 Szablony danych • Różnicowanie szablonów danych: <DataTemplate x:Key="BookDataTemplate"> <Border ... Background="{Binding Path=Price, Converter={StaticResource PriceToBackgroundConverter}}"> ... </Border> </DataTemplate> 53/85 Szablony danych 54/85 Szablony danych • 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"); } } } 55/85 Szablony danych <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> 56/85 Szablony danych 57/85 Szablony danych • 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); 58/85 if (property.GetValue(product, null).ToString() == PropertyValueToHighlight) { return HighlightTemplate; } else { return DefaultTemplate; } } } 59/85 Szablony danych <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; 60/85 Zmiana układu listy • 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> 61/85 Zmiana układu listy 62/85 Widoki danych – Data Views • • • • 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); 63/85 Widoki danych – filtrowanie • 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); }; 64/85 Widoki danych – filtrowanie 65/85 Widoki danych – filtrowanie 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; } } 66/85 Widoki danych – filtrowanie 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; 67/85 Widoki danych – filtrowanie • Uwaga: nie można łączyć kilku filtrów – należy raczej zaprojektować filtr z wieloma warunkami. 68/85 Widoki danych – sortowanie • Sortowanie na podstawie wskazanej własności danych: ListCollectionView view = (ListCollectionView)CollectionViewSource.GetDefaultView(lista .ItemsSource); view.SortDescriptions.Add(new SortDescription("Title", ListSortDirection.Ascending)); 69/85 Widoki danych – sortowanie 70/85 Widoki danych – sortowanie • 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(); 71/85 Widoki danych – sortowanie 72/85 Widoki danych – grupowanie • Jest zbliżone do sortowania: view.GroupDescriptions.Add(new PropertyGroupDescription("Author")); 73/85 Widoki danych – grupowanie 74/85 Widoki danych – grupowanie • • 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ę 75/85 Widoki danych – grupowanie <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. 76/85 Widoki danych – grupowanie 77/85 Widoki danych – grupowanie • 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; 78/85 if (price < GroupInterval) { 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."); } } 79/85 Widoki danych – grupowanie view.SortDescriptions.Add(new SortDescription("Price", ListSortDirection.Ascending)); PriceRangeProductGrouper grouper = new PriceRangeProductGrouper(); grouper.GroupInterval = 10; view.GroupDescriptions.Add(new PropertyGroupDescription("Price", grouper)); 80/85 Widoki danych – grupowanie 81/85 Widoki danych – nawigacja • • 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> 82/85 Widoki danych – nawigacja • 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; 83/85 Widoki danych – nawigacja 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(); } 84/85 Widoki danych – nawigacja 85/85