Filtrowanie danych

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”

  1. Pingback: dotnetomaniak.pl

Comments are closed.