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!

Reklamy

Jedna uwaga do wpisu “Filtrowanie danych”

  1. Pingback: dotnetomaniak.pl

Możliwość komentowania jest wyłączona.