Po wprowadzeniu InvoiceInvoker w stadium pre-pre-beta, zająłem się dodawaniem kolejnych modeli, widoków i kontrolerów. Jak na razie projekt uzupełniłem o stronę klientów (bazującą na tabeli RegisteredCustomers bazy danych i analogiczną to strony produktów, więc niestanowiącą wyzwania). Strony faktur i ich szablonów wymagać będą przemyślenia, dlatego zostawiam je na deser. Dziś natomiast przybliżę sposób wyłuskiwania interesujących użytkownika pozycji z nieprzebranego mrowia produktów i niezliczonych szeregów klientów – w skrócie: filtrowania danych.
Filtrowanie udostępniane przez repozytoria
Opisując implementację wzorca repozytorium w warstwie dostępu do danych, zwróciłem uwagę na metodę GetByExpression, pozwalającej na pobranie obiektów spełniających dowolne warunki.:
(tutaj dla repozytorium klientów)
public List<RegisteredCustomer> GetByExpression(Func<RegisteredCustomer, bool> expression) { using (MainDataContext dataContext = new MainDataContext(_connectionString)) { var mainQuery = from customer in dataContext.RegisteredCustomers where customer.RegisteredSellerId == _registeredSellerId // metody grupowej selekcji zwracają encje przypisane konkretnemu sprzedawcy orderby customer.CompanyName ascending select customer; return mainQuery.Where(expression).ToList(); } }
Postanowiłem udostępnić użytkownikowi filtrowanie klientów po imieniu, nazwisku i nazwie firmy, a produktów po nazwie i cenie netto (przez zdefiniowanie ceny minimalnej i maksymalnej).
Konstrukcja filtrów
Dla filtru produktów stworzyłem klasę przechowującą filtrowane pola i konstruującą z nich wyrażenie lambda, przekazywane wspomnianej przed chwilą metodzie repozytorium:
public class ProductsFilter { [DisplayName("Nazwa")] public string Name { get; set; } [DisplayName("Min. cena netto")] [RegularExpression("^[0-9]+(,[0-9])?[0-9]*$")] public string MinPrice { get; set; } [DisplayName("Maks. cena netto")] [RegularExpression("^[0-9]+(,[0-9])?[0-9]*$")] public string MaxPrice { get; set; } public Func<RegisteredProduct, bool> GetExpression() { string name = Name ?? ""; decimal minPrice; decimal maxPrice; decimal.TryParse(MinPrice, out minPrice); if (decimal.TryParse(MaxPrice, out maxPrice) == false) maxPrice = decimal.MaxValue; return product => product.Name.ToLower().StartsWith(name.ToLower()) && product.NetPrice >= minPrice && product.NetPrice <= maxPrice; } }
W przypadku filtrowania po nazwie, zgodność początku nazwy produktu z podanym przez użytownika ciągiem znaków (wielkość liter nie ma znaczenia) wystarczy, aby produkt został wyświetlony.
Filtr klientów posiada tylko jedno (dlatego nie utworzyłem jego klasy) kryterium: NameFilter. Akceptuje klienta, jeśli podany przez użytkownika ciąg znaków jest początkiem jego imienia (lub któregoś z imion), nazwiska albo nazwy firmy. Wielkość liter i tutaj nie ma znaczenia. Wyrażenie lambda tego filtru wygląda więc tak:
Func<RegisteredCustomer, bool> expression = customer => customer.CustomerName.ToLower().Split(' ').Any(x => x.StartsWith(NameFilter.ToLower())) || customer.CompanyName.ToLower().StartsWith(NameFilter.ToLower());
(Zaznaczona linia dzieli nazwę (imię i nazwisko) klienta na słowa, w których szuka tych akceptowanych przez filtr.)
Implementacja w kontrolerach
Filtrowanie i wyświetlanie pozycji udostępniają akcje (i widoki) Index stron klientów i produktów. Korzystają one z odpowiednich modeli:
(dla strony klientów)
public class CustomersIndexViewModel { [DisplayName("Imię i nazwisko lub nazwa firmy")] public string NameFilter { get; set; } // wspomniane pole filtru klientów public List<RegisteredCustomer> Customers { get; set; } // lista klientów }
(dla strony produktów)
public class ProductsIndexViewModel { public ProductsFilter Filter { get; set; } // filtr produktów public List<RegisteredProduct> Products { get; set; } // lista produktów }
Akcje wyglądają więc tak:
(dla strony klientów)
public ActionResult Index(CustomersIndexViewModel viewModel) { string nameFilter = viewModel.NameFilter ?? ""; Func<RegisteredCustomer, bool> expression = customer => // pokazane wcześniej zapytanie customer.CustomerName.ToLower().Split(' ').Any(x => x.StartsWith(nameFilter.ToLower())) || customer.CompanyName.ToLower().StartsWith(nameFilter.ToLower()); viewModel.Customers = _repository.GetByExpression(expression); // _repository - prywatne repozytorium klientów return View(viewModel); }
(dla strony produktów)
public ActionResult Index(ProductsIndexViewModel viewModel) { if (viewModel.Filter == null) viewModel.Filter = new ProductsFilter(); viewModel.Products = _repository.GetByExpression(viewModel.Filter.GetExpression()); // _repository - prywatne repozytorim produktów return View(viewModel); }
Implementacja w widokach
Pozostało jeszcze tylko w odpowiednich widokach wyświetlić po kilka pól tekstowych i po dwa przyciski: aktywujące bądź czyszczące filtry. W widoku klientów wygląda to tak:
<% using (Html.BeginForm()) { %> <fieldset> <legend>Filtr</legend> <%: Html.LabelFor(model => model.NameFilter) %>: <!-- etykieta pola filtru --> <%: Html.TextBoxFor(model => model.NameFilter) %> <!-- pole tekstowe pola filtru --> <input type="submit" value="Filtruj" /> | <%: Html.ActionLink("Wyczyść", "Index") %> </fieldset> <% } %>
W widoku produktów, tak:
<% using (Html.BeginForm()) { %> <fieldset> <legend>Filtr</legend> <%: Html.LabelFor(model => model.Filter.Name) %>: <!-- etykieta pola nazwy --> <%: Html.TextBoxFor(model => model.Filter.Name) %> <!-- pole tekstowe nazwy --> <%: Html.LabelFor(model => model.Filter.MinPrice) %>: <!-- etykieta pola ceny minimalnej --> <%: Html.TextBoxFor(model => model.Filter.MinPrice) %> <!-- pole tekstowe ceny minimalnej --> <%: Html.LabelFor(model => model.Filter.MaxPrice) %>: <!-- etykieta pola ceny maksymalnej --> <%: Html.TextBoxFor(model => model.Filter.MaxPrice) %> <!-- pole tekstowe ceny maksymalnej --> <input type="submit" value="Filtruj" /> | <%: Html.ActionLink("Wyczyść", "Index") %> </fieldset> <% } %>
Tym sposobem dałem użytkownikowi możliwość usunięcia sprzed swych szanownych oczu nieinteresujących go pozycji. Za kilka dni dam mu również możliwość definiowania szablonów faktur. Zapraszam!
1 thought on “Filtrowanie danych”
Comments are closed.