InvoiceInvoker a świat wielkiej polityki i ekonomii

Tytuł wpisu zwiastuje treść i tematykę poważną i podniosłą, jednak bez obaw – poruszana kwestia nie będzie ściśle polityczna czy ekonomiczna, ani daleko odbiegająca od problemów, z którymi zdarza się borykać programistom. Chodzi mianowicie o dostosowanie aplikacji do obowiązujących przepisów, w tym przypadku – stawek procentowych podatku VAT.
Kilka dni po mojej decyzji o starcie w konkursie Daj się poznać z projektem InvoiceInvoker, zapowiedziano zmianę wspomnianych stawek. Był to chyba pierwszy przypadek wywarcia przez urzędujące władze bezpośredniego wpływu na moje decyzje. Ściślej – decyzje projektowe. Dziś, kiedy już prace nad projektem są właściwie na ukończeniu, zaprezentuję tych decyzji konsekwencje – które, co oczywiste dla projektu programistycznego, przybrały postać linii kodu. Przyznaję jednocześnie, że tematyka tego wpisu jest podyktowana moją niechęcią do ciągłego pisania o co raz to nowych widokach i kontrolerach (choć opisanie finalnych widoków i kontrolerów, tych dotyczących wystawiania faktur, wydaje się być nieuniknione).
Przejdźmy jednak do rzeczy. Podstawową decyzją było jak największe uniezależnienie kodu programu od obowiązujących stawek VAT. Początkowo, wprowadzenie tych stawek chciałem zrzucić na użytkownika i umożliwić łatwą ich edycję (na wypadek, gdyby w przyszłości znowu zostały zmienione). Takie rozwiązanie wymagałoby jednak dodania kolejnej kolumny do tabeli sprzedawców w bazie danych, dlatego ostatecznie postanowiłem utworzyć pole VatRates w pliku konfiguracyjnym aplikacji, odpowiedzialne za przechowywanie rzeczonych stawek VAT:

<configuration>
	<appSettings>
		<add key="VatRates" value="22%, 7 %, 3 %, 0 %, n.p., z.w." />
		<add key="PaymentTypes" value="Przelew, Gotówka, Karta płatnicza" /> <!-- swoją drogą, podobnie postąpiłem w kwestii sposobów płatności -->
	</appSettings>

Dostanie się do tego pola i zwrócenie listy stawek nie jest zatem skomplikowane:

public static List<string> GetVatRates()
{
	string vatRates = ConfigurationManager.AppSettings["VatRates"];
	string[] separator = { ", ", "," };
	return vatRates.Split(separator, StringSplitOptions.RemoveEmptyEntries).ToList();
}

Wszystko to jednak tylko ciągi znaków, a przecież głównym ich przeznaczeniem jest reprezentowanie liczb, przez które nalezy pomnożyć wartość netto, by uzyskać wartość brutto produktu. Wyłuskiwaniem tychże liczb zajmuje się następująca metoda:

public static int GetVatPercentage(string vatRate)
{
	int result = 0;
	string vatPercentage = string.Empty;

	if (vatRate.EndsWith(" %")) // ciąg znaków reprezentujący stawkę procentową VAT może kończyć się tylko znakami " %" lub "%"
		vatPercentage = vatRate.Substring(0, vatRate.Length - 2);
	else if (vatRate.EndsWith("%"))
		vatPercentage = vatRate.Substring(0, vatRate.Length - 1);

	// jeśli stawka VAT nie zawiera procentów (jak np. "z.w." - produkt zwolniony od podatku), to jej wartość wynosi 0

	int.TryParse(vatPercentage, out result);

	return result;
}

Wyliczenie wartości podatku VAT odprowadzonego od produktu i warości brutto tego produtku, przy znanej warości netto, wygląda więc tak:

// przedstawione wcześniej metody znajdują się w statycznej klasie VatRatesProvider
int vatPercentage = VatRatesProvider.GetVatPercentage(product.VatRate); //właściwość VatRate jest ciągiem znaków zawierającym stawkę VAT produktu
product.VatValue = product.NetValue * (vatPercentage * 0.01M); // NetValue - wartość netto produktu, VatValue - warość podatku
product.GrossValue = product.NetValue + product.VatValue; // GrossValue - wartość brutto produktu

Metody GetVatRates natomiast używam do przekazania stawek VAT widokom tworzenia i edycji produktów.

I to wszystko w tym temacie, przedstawione rozwiązanie nie jest skomplikowane, ale chyba warte opisania.
Na koniec małe usprawiedliwienie. Wraz z nadejściem (a niedawno także odejściem) ostatnich dni wakacji, częstotliwość pojawiania się nowych notek zauważalnie spadła, czego powodem, jak łatwo się domyślić, jest natłok wszelakich zajęć różnych od kodowania. Projektowi nie grozi jednak, jeśli dobrze rozumiem regulamin i FAQ konkursu, dyskwalifikacja: jeśli dobrze rozumiem regulamin i FAQ konkursu, bardziej niż częstotliwość wpisów, liczy się ich ilość – która do piętnastego listopada powinna liczyć co najmniej dzwadzieścia.

Reklamy

Starcia runda druga: produkty na szablonie faktury

Ostatnio opisałem rozwiązanie pierwszej części mojego problemu. Po tygodniowej przerwie, wypełnionej korzystaniem z ostatnich dni wakacji, wracam na ring i ostatecznie nokautuję przeciwnika na trzech płaszczyznach.

1. HTML
Pierwszą z nich jest struktura tebeli produktów (szczegóły w punkcie drugim listy umieszczonej w akapicie O co chodziło? poprzedniego wpisu). Składają się na nią następujące wiersze:
– nagłówek tabeli:

<table>
	<tr>
		<th>Nazwa</th>
		<th>PKWiU</th>
		<th title="Jednostka miary">J. miary</th>
		<th>Cena netto</th>
		<th>Stawka VAT</th>
		<th style="min-width: 52px"></th>
	</tr>

– produkty juz zawarte w szablonie (istotne przy edycji już istniejącego szablonu):

<% foreach (var item in Model.Products) { %> <!-- Model - szablon faktury, Products - produkty zawarte w szablonie -->
<tr id="product-<%: item.Id %>"> <!-- każdy wiersz ma odpowiedni identyfikator -->
	<td><%: item.Name %></td>
	<td align="center"><%: item.Pkwiu %></td>
	<td><%: item.MeasureUnit %></td>
	<td align="right"><%: item.NetPrice.ToString("N") %></td>
	<td align="right"><%: item.VatRate %></td>
	<td>
		<input type="button" value="Usuń" onclick="removeProduct(<%: item.Id %>)" /> <!-- przycisk usuwania produktu, funkcję removeProduct pokażę w akapicie dotyczącym jQuery -->
	</td>
</tr>
<% } %>

– pusty wiersz, w miejsce którego wstawiane będą dodawane przez użytkownika produkty:

<tr id="addedProduct"></tr>

– wiersz umożliwiający dodanie produktu do szablonu:

	<tr id="newProduct">
		<td>
			<input type="text" id="newProduct-name" style="width: 130px" /> <!-- pole tekstowe umożliwiające dodanie produktu -->
		</td>
		<td class="transparentRB"></td> <!-- komórka o niewiczonej prawej i dolnej krawędzi -->
		<td class="transparentLRB"></td> <!-- komórka o niewiczonej lewej, prawej i dolnej krawędzi -->
		<td class="transparentLRB"></td>
		<td class="transparentLRB"></td>
		<td class="transparentLRB"></td>
	</tr>
</table>

Całość, dla szablonu z już dodanymi produktami, prezentuje się tak:

2. C#
Skoro uzyskałem wygląd tabeli, mogę zabrać się za napisanie kilku metod C#, z których korzystać będą (poprzez AJAX) funckje jQuery. Interakcja z użytkownikiem wygląda tak:
1. Użytkownik wpisuje początek nazwy produktu, który zamierza dodać do szablonu,
2. Dzięki Autocomplete i metodzie FilterProducts, wyświetlana jest lista podpowiedzi,
3. Użytkownik wybiera produkt z listy,
4. Wywoływana jest metoda AddProduct,
5. Użytkownik, jeśli chce, usuwa produkt (przycisk Usuń),
6. Po wciśnięciu przycisku Usuń wywoływana jest metoda RemoveProduct.

Pominąłem tutaj rolę jQuery – opiszę ją w kolejnym paragrafie. Wspomnę na razie tylko o wielce wygodnym pluginie, jakim jest Autocomplete (używam jego zmodyfikowanej wersji). Odpowiada on dokładnie za to, czego potrzebuję: wyświetla podpowiedzi pod polem tekstowym. W tym miejscu jedyną istotną informacją dotyczącą Autocomplete jest to, że, komunikując się z kontrolerem, przekazuje mu treść pola tekstowego (w tym przypadku początek nazwy produktu) w parametrze o nazwie „q”, a przyjmuje ciąg znaków zawierający kolejne pozycje listy podpowiedzi (oddzielone znakiem ‚\n’). Metoda filtrowania produktów, wymieniona w punkcie drugim przedstawionej wyżej listy, prezentuje się więc tak:

public ActionResult FilterProducts(string q)
{
	List<RegisteredProduct> products = _productRepository.GetByName(q);
	string[] contents = new string[products.Count];

	for (int i = 0; i != products.Count; i++)
		contents[i] = products[i].Name + "|" + products[i].Id.ToString(); // znak '|' oddziela treść pola listy od dodatkowych, przypisanych do niego, wartości

	return Content(string.Join("\n", contents));
}

Listę produktów zawartych w szablonie, a właściwie ich identyfikatorów, przechowuję w danych sesji (Session[„ProductIds”]). Dodawanie i usuwanie produktów opiera się zatem głównie na operacjach na tej liście. Metoda dodawania zwraca ponadto dane dodawanego produktu:

public ActionResult AddProduct(int id)
{
	List<int> productIds = Session["ProductIds"] as List<int>; // pobranie wspomnianej listy identyfikatorów

	if (id == 0 || productIds.Contains(id))
		return Json(null); // wielokrotne dodawanie tego samego produktu nie jest dozwolone

	productIds.Add(id);

	RegisteredProduct product = _productRepository.GetById(id); // _productRepository - prywatne repozytorium produktów

	var data = new // dane dodawanego produktu
	{
		Id = product.Id,
		Name = product.Name,
		Pkwiu = product.Pkwiu,
		MeasureUnit = product.MeasureUnit,
		NetPrice = product.NetPrice.Value.ToString("N"),
		VatRate = product.VatRate
	};

	return Json(data);
}

[HttpPost]
public ActionResult RemoveProduct(int id)
{
	List<int> productIds = Session["ProductIds"] as List<int>;
	productIds.Remove(id);

	return null;
}

Teraz pozostaje tylko spiąć sztywny kod HTML i nie-tak-łatwo-dostępne-z-poziomu-html metody C# za pomocą:

3. jQuery
Na tej płaszczyźnie nie pojawi się chyba nic, czego nie opisałem w poprzednim wpisie (no dobrze, pojawi się jedno słowo: autocomplete). Aby jednak zwieńczyć dzieło, pokażę poszczególne funkcje. Na początek kilka linijek ułatwiających dostęp do metod kontrolera:

var url_FilterProducts = '<%: Url.Action("FilterProducts", "PatternEditor") %>';
var url_AddProduct = '<%: Url.Action("AddProduct", "PatternEditor") %>';
var url_RemoveProduct = '<%: Url.Action("RemoveProduct", "PatternEditor") %>';

Wspomniany już kilkukrotnie plugin Autocomplete zaimplementowałem w ten sposób:

$("#newProduct-name") // selekcja pola tekstowego nazwy dodawanego produktu
	.autocomplete(
		url_FilterProducts, // ścieżka do metody zwracającej listę podpowiedzi
		{
			delay: 50, // lista podpowiedzi pojawi się 50ms po wpisaniu znaku w polu tekstowym
			onItemSelect: function (li) { // funkcja wywoływana po wybraniu podpowiedzi, pobiera pole listy (li)
				$.post(url_AddProduct, { "id": li.extra[0] }, handleProductAddition); // użycie AJAX do wywołania metody AddProduct kontrolera, następnie wywołanie funkcji handleProductAddition
				}
		}
	);

Występująca w powyższym kodzie funkcja handleProductAddition odpowiada za dodanie produktu do tabeli – wstawia nowy wiersz w przygotowane na to miejsce (patrz addedProduct w paragrafie HTML). Jej treść wygląda tak:

function handleProductAddition(data) { // funkcja pobiera dane produktu
	if (data) { // patrz szósta linia metody AddProduct (paragraf "C#")
		$("#addedProduct").replaceWith( // wstawienie danych produktu w miejsce pustego wiersza
		'<tr id="product-' + data.Id + '">' + // wiersz otrzymuje swój identyfikator
			'<td>' + data.Name + '</td>' +
			'<td align="center">' + data.Pkwiu + '</td>' +
			'<td>' + data.MeasureUnit + '</td>' +
			'<td align="right">' + data.NetPrice + '</td>' +
			'<td align="right">' + data.VatRate + '</td>' +
			'<td>' +
				'<input type="button" value="Usuń" onclick="removeProduct(' + data.Id + ')"/>' +
				// kliknięcie przycisku wywołuje funkcję removeProduct, jako parametr podając identyfikator produktu, a tym samym wiersza tabeli
			'</td>' +
		'</tr>' +
		'<tr id="addedProduct"></tr>' // pusty wiersz zostaje, przyda się do dodania następnego produktu
		);
	}
	else { // wielokrotne dodawanie tego samego produktu nie jest dozwolone
		showProductError("Ten produkt został już dodany"); // funkcja showProductError wyświetla treść błędu pod tabelą produktów
	}

	$("#newProduct-name").val(""); // wyczyszczenie treści pola tekstowego
	$("#newProduct-name").focus(); // focus pozostaje na polu tekstowym
}

Usuwanie produktu odbywa się o wiele prościej. Po kliknięciu przez użytkownika przycisku Usuń, wywoływana jest następująca funkcja:

function removeProduct(id) { // funkcja pobiera identyfikator usuwanego produktu
	$.post(url_RemoveProduct, { "id": id }); // wywołanie metody RemoveProduct kontrolera (identyfikator produktu przekazywany jest przez parametr o nazwie "id")
	$("tr[id=product-" + id + "]").remove(); // selekcja i usunięcie wiersza zawierającego dane produktu
}

KO!
Uff, to wszystko. Działanie kodu w praktyce można sprawdzić po ściągnięciu stąd. Pojedynek nie był może tak widowiskowy jak walki MMA, jednak liczy się to, że przeciwnik leży na deskach. Mogę teraz przejść nad nim, by zmierzyć się z finalnym bossem – wystawianiem faktur.

Wieczorek zapoznawczy z jQuery, czyli get the $(„# in!

Po kilkudniowej nieobecności wśród cywilizacji, wracam do pracy. Dziś przedstawię rozwiązanie przedstawionego ostatnio problemu.

O co chodziło?
Chodziło o szablony faktur. Dodaję tę funkcjonalność po to, aby użytkownik, jeśli zdarza mu się wystawiać co miesiąc identyczne faktury, nie musiał tworzyć każdej z nich od zera. Szablon przechowuje następujące informacje:
– odbiorca faktury (klient),
– produkty, na które zostanie wystawiona faktura,
– sposób płatnośći (przelew, gotówka itp.),
– waluta płatnośći,
– ewentualne uwagi.
Tworząc widok edycji szablonów, napotkałem na przeszkodę. Chciałem w nim umieścić dwa elementy wymagające wymiany danych między widokiem i jego kontrolerem, bez konieczności odświeżania całej strony:

  1. rozwijana lista klientów – po wyborze klienta, jego dane są wyświetlane w tabeli,
  2. lista produktów – tabela zawierająca informacje o wybranych produktach (z możliwością ich usuwania) i umożliwiająca dodanie produktów do listy (pole tekstowe z zaimplementowanym autouzupełnianiem).

Moja wiedza nie była wystarczająca, by podołać takiemu wyzwaniu, dlatego musiałem ją poszerzyć.

Write less, do more!
Nagłówek tego akapitu to motto biblioteki JavaScript, z którą postanowiłem się zapoznać: jQuery (podlinkowana strona jest godna gorącego polecenia, zawiera absolutnie wszystko, czego potrzeba do nauki). Nie miałem nigdy do czynienia z JavaScript, dlatego początkowo trudno było mi rozczytywać przykłady z dokumentacji (zwłaszcza, że niejedna linijka kodu w nich zawartego zaczyna się od zlepka $(„#, widniejącego w tytule wpisu). Opanowanie podstaw nie okazało się jednak trudne, po kilku godzinach przeszedłem więc do zagadnienia, które interesowało mnie najbardziej, czyli sposobu wykorzystania w jQuery AJAX. I tu nie napotkałem większych trudności, dlatego zabrałem się za ostatni (po znalezieniu rozwiązania i zdobycia odpowiedniej wiedzy) etap walki z moim problemem: kodowanie.

Ostatnie starcie: runda pierwsza
Metody zwracające obiekty JSON postanowiłem oddzielić od tych (umieszczonych w kontrolerze PatternsController) zwracających konkretne widoki, dlatego stworzyłem nowy kontroler: PatternEditorController. Następnie zacząłem komponować opisany wyżej widok edycji szablonów faktur. Na pierwszy ogień poszła realizacja pierwszej z wypunktowanych funkcjonalności: lista klientów i tabela danych wybranego klienta. Garść linii kodu HTML zdefiniowało wygląd tworzonego elementu:

(pochyloną czcionką wpisałem identyfikatory odpowiednich komórek tabeli)
Kilkanaście linii JavaScript, przy pomocy jQuery, narzuciło zachowanie listy customer-name:

var url_GetCustomer = '<%: Url.Action("GetCustomer", "PatternEditor") %>'; // URL do odpowiedniej akcji zwracającej obiekt JSON

$("#customer-name") // selekcja elementu o identyfikatorze customer-name
	.change( // zdarzenie zgłaszane po wyborze którejś z pozycji listy
		function () { // funckja anonimowa wywoływana po zgłoszeniu zdarzenia
			$.post(url_GetCustomer, { "id": this.value }, handleCustomerUpdate); // wykorzystanie AJAX (http://api.jquery.com/jQuery.get/)
		}
	);

function handleCustomerUpdate(customer) {
	// wypełnienie zawartości odpowiednich komórek tabeli
	$("#customer-street").text(customer.Street);
	$("#customer-city").text(customer.City);
	$("#customer-postalCode").text(customer.PostalCode);
	$("#customer-nip").text(customer.Nip);
}

Natomiast pojedyncza metoda C# przejęła odpowiedzialność za pobranie danych klienta i przekazanie ich do widoku:

[HttpPost]
public ActionResult GetCustomer(int id)
{
	RegisteredCustomer customer = _customerRepository.GetById(id); // _customerRepository - prywatne repozytorium klientów

	var data = new
	{
		CustomerName = customer.CustomerName,
		CompanyName = customer.CompanyName,
		Street = customer.Street,
		City = customer.City,
		PostalCode = customer.PostalCode,
		Nip = customer.Nip
	};

	return Json(data);
}

Efekt jest widoczny na załączonym obrazku:

Póki co, ja 1 : 0 MVC 2.

Jak widać, osiągnięcie żądanego efektu okazało się nie być wcale trudne i skomplikowane. Wystarczyło kilka(naście?) godzin, abym mógł dopisać jQuery do dość długiej już listy technologii (choć może w tym przypadku to zbyt duże słowo) poznanych podczas pracy nad projektem. W kolejnym wpisie zrelacjonuję drugą, nieco bardziej skomplikowaną, rundę tego starcia – realizację tabeli produktów. Zapraszam!


EDIT: Ostatnie wakacyjne wyjazdy pochłonęły sporo mojego wolnego czasu, stąd w tym i poprzednim tygodniu tylko po jednym wpisie. Zgodnie z regulaminem konkursu (i jego FAQ), te dwa tygodnie traktuję jako jeden tydzień pracy i jeden „urlopu”. Wykorzystałem zatem drugi z pięciu przysługujących tygodni wolnego.

Przestój w kodowaniu i prezentacja wyglądu aplikacji

Po stworzeniu stron produktów, klientów i profilu użytkownika, zamierzałem rzutem na taśmę dodać strony faktur i ich szablonów. Okazało się to jednak trudniejsze niż myślałem.

Przedstawienie problemu
Jako pierwsza, miała powstać strona szablonów faktur. Scenariusze przeglądania listy istniejących szablonów, usuwania ich, a także przeglądania ich szczegółów, są analogiczne do ich odpowiedników na stronach produktów i klientów, dlatego łatwe to zrealizowania. Problematyczne okazało się jednak dodawanie i edytowanie szablonów – realizowane w jednym widoku (partial view): PatternEditor.ascx. Chciałem w nim umieścić następujące elementy:

  • rozwijana lista klientów – po dokonaniu wyboru, w przygotowanej tabelce powinny zostać wyświetlone dane wybranego klienta,
  • lista produktów z możliwością dodawania i usuwania pozycji – zrealizowana w formie tabeli, której ostatni wiersz zawiera pole tekstowe (pod którym, po wpisaniu przez użytkownika początku nazwy produktu, wyświetlona zostanie lista podpowiedzi) i przycisk umożliwiający dodanie wybranego produktu
  • rozwijana lista dostępnych form płatności,
  • pole tekstowe przeznaczone do wpisania waluty płatności,
  • pole tekstowe przeznaczone do wpisania ewentualnych uwag do faktury.

O ile trzy ostatnie punkty to (od dwóch tygodni) nic nowego, o tyle dwa pierwsze wymagają głębszego zanurzenia się w ASP.NET MVC – poznania sposobu na wymianę danych między widokiem a kontrolerem bez konieczności odświeżania strony. Pierwsze, co mi przyszło na myśl, to oczywiście AJAX. Tutorial, z którego korzystam, wspomina co prawda o nim i pokazuje jeden przykład wykorzystania, ale to za mało.

Co się odwlecze…
Przez ostatnie kilka dni byłem odcięty od sieci – więc i od świata. Mogłem po omacku szukać rozwiązania, lub zająć się wykorzystaniem już posiadanej wiedzy i oprogramować przeglądanie i usuwanie szablonów – wybrałem to drugie. Zorientowałem się jednocześnie, że jeszcze w żadnym miejscu na blogu nie zaprezentowałem interfejsu mojej aplikacji. Co prawda, spora część widoków i prawie cały arkusz stylów to kod wygenerowany automatycznie, jednak z „dziennikarskiego obowiązku” pokażę kilka zrzutów ekranu:
[dane klientów i produktów są zupełnie nieistotne]
lista produktów
tworzenie nowego klieta
lista szablonów faktur
szczegóły szablonu faktury

Zanosi się na to, że w tym tygodniu nie zdążę już nic merytorycznego napisać. W przyszłym, mam nadzieję, uda mi się opisać rozwiązanie problemu stworzenia opisanego wcześniej widoku PatternEditor. Po krótkim przeglądzie dostępnych technologii, mam już chyba kandydata na miarę moich potrzeb: jQuery. Ale o tym w następnym odcinku. Zapraszam!

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!

Exodus

Na początek zapowiedzniane w poprzednim wpisie kilka linijek o rozwiązaniu problemu z automatyczną walidacją danych – a właściwie jej brakiem. Rozwiązaniem okazała się być zmiana nagłówka metody kontrolera z takiego:
(generowanego automatycznie, tutaj dla scenariusza tworzenia nowego produktu)

[HttpPost]
public ActionResult Create(FormCollection collection)

na taki:

[HttpPost]
public ActionResult Create(ProductsViewModel viewModel)

gdzie ProductsViewModel jest klasą, na której bazuje silnie typowany widok dodawania produktu:

public class ProductsViewModel
{
	public ProductModel Product { get; set; } // patrz typ RegisteredProductModel przedstawiony w poprzednim wpisie
	public List<string> VatRates { get; set; } // stawki VAT możliwe do przypisania produktowi
}

Tak jak się spodziewałem – banał. Coż, tak bywa z rzeczami na tyle prostymi, że nie są jasno omówione w tutorialach. Przejdźmy jednak do tematu przewodniego dzisiejszego wpisu:

Pierwsza uruchamialna wersja programu
Po pięciu tygodniach pracy mogę wreszcie sprawdzić działanie kodu w praktyce – nie tylko, jak dotychczas, poprzez testy jednostkowe. Przez kilka dni nauki maglowałem tabelę RegisteredProducts bazy danych. Stworzyłem jej model, kontoler i widoki, realizujące przeglądanie, dodawanie, edytowanie i usuwanie produktów. Następnie wziąłem się za mechanizm rejestracji i logowania użytkowników. Jest on implementowany automatycznie, więc pozostało mi tylko zapoznać się z jego działaniem.
Dopiero teraz mogłem przemyśleć sposób przypisywania encji bazy danych konkretnym użytkownikom. Jako że nazwa użytkownika jest unikalna, zdecydowałem się na dodanie do tabeli RegisteredSellers kolumny UserName (co widać na załączonym w poprzednim akapicie schemacie) i używania jej jako łącznika między użytkownikiem, a encją. Po zarejestrowaniu nowego użytkownika, do bazy danych dodawany jest nowy obiekt RegisteredSeller – z jego nazwą umieszczoną w polu UserName.
Zarejestrowany użytkownik jest przenoszony na stronę swojego profilu, gdzie może podać dane takie jak imię, nazwisko, czy nazwa i adres firmy. Ma też do dyspozycji wspomnianą wcześniej stronę produktów, z możliwością filtrowania po nazwie i cenie. Niezalogowani użytkownicy mogą zobaczyć jedynie stronę O programie. Na razie to wszystko, aplikację w obecnej formie można pobrać tutaj.
Należałoby jeszcze wspomnieć o sposobie przechowywania (podczas sesji) informacji o zalogowanym użytkowniku. W dowolnym (oprócz konstruktorów) miejscu kontrolerów mam dostęp do jego nazwy:

string userName = User.Identity.Name;

Bardziej przydałby mi się jednak identyfikator encji RegisteredSeller powiązanej z użytkownikiem. Do repozytorium sprzedawców (RegisteredSellerRepository) dodałem więc taką metodę:

public int GetIdByUserName(string userName)
{
	using (MainDataContext dataContext = new MainDataContext(_connectionString))
	{
		return dataContext.RegisteredSellers.SingleOrDefault(seller => seller.UserName == userName).Id;
	}
}

Kontroler produktów uzupełniłem natomiast o metodę wywoływaną przed wykonaniem każdej akcji:

protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
	if (Session["SellerId"] == null)
		Session["SellerId"] = new RegisteredSellerRepository().GetIdByUserName(User.Identity.Name);

	_sellerId = (int)Session["SellerId"]; // _sellerId - prywatne pole
	_repository = new RegisteredProductRepository(_sellerId); // _repository - prywatne pole

	base.OnActionExecuting(filterContext);
}

Zaznaczony fragment będzie powtarzał się w każdym kontrolerze, a wykorzystany zostanie podczas pojedynczej sesji tylko raz, przy wykonaniu pierwszej w programie akcji, dlatego takie rozwiązanie niezbyt mi się podoba. Na razie nie znalazłem jednak lepszego – może Czytelnicy takie znają?

Na chleb – „pep”, a na ASP – „a-ef-e”, czyli raczkowanie w ASP.NET MVC

Mój tort jest już prawie gotowy: spodni biszkopt równomiernie nasiąknął danymi, a środkowa masa osiągnęła spójną konsystencję. Pozostało jeszcze tylko przygotować ostatnią warstwę, dzięki której całość będzie prezentować się iście smakowicie.
Ale dość naśladowania mistrza – przedmiot dzisiejszego wpisu wymaga klarownego języka.

Model – Vidok – Contoler
Struktura MVC polega na podziale pracy aplikacji na trzy segmenty:
– Model – dostarcza informacje o danych, na których program będzie pracować,
– View – odpowiada za formę, czyli to, co widzi użytkownik,
– Controller – realizuje interakcję z użytkownikiem i przepływ danych z Model do View.

Tę właśnie strukturę realizuje Microsoft ASP.NET MVC. Przystępne omówienie tej technologii można znaleźć u źródeł.

Pierwsze kroki
Jak dotąd tworzyłem jedynie programy desktopowe. Aplikacje sieciowe są dla mnie nowością, dlatego skorzystałem z życzliwiości wuja Google i, oprócz informacji teoretycznych, znalazłem sensowanie wyglądający tutorial, z którego postanowiłem skorzystać. Po krótkim przygotowaniu teoretycznym, stworzyłem próbny projekt i wziąłem się za praktykę. Wstępne ujarzmianie kontrolerów i widoków poszło gładko, problem pojawił się natomiast przy próbie wykorzystania projektu InvoiceInvoker jako modelu. Treść problemu była taka: „SQL Server Compact is not intended for ASP.NET development”. Moja reakcja: taka. Nie pozostało nic innego, jak stworzyć nową bazę danych o tych samych tabelach, tym razem używając „normalnej” edycji SQL Server, i ruszyć dalej. Udało mi się oprogramować scenariusze przeglądania, dodawania, edytowania i usuwania produktów (tabela RegisteredProducts bazy danych). Moją euforię zgasił jednak kolejny problem, tym razem niewynikający (mam nadzieję) bezpośrednio z mojego niedopatrzenia: nie potrafię skłonić aplikacji do przeprowadzania walidacji danych wprowadzanych przez użytkownika. Jako że model bazy danych znajduje się w bibliotece zewnętrznej (InvoiceInvoker.Logic.dll), stworzyłem zastępczy model produktu:

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using InvoiceInvoker.Logic;

namespace MvcApplication1.Models
{
	public class RegisteredProductModel
	{	
		public int Id { get; set; }
		public int RegisteredSellerId { get; set; }

		[DisplayName("Nazwa")]
		[Required(ErrorMessage="Name required")]
		[StringLength(60, ErrorMessage="Name must be under 60 characters")]
		public string Name { get; set; }

		[DisplayName("PKWiU")]
		[StringLength(20, ErrorMessage = "PKWiU must be under 20 characters")]
		public string Pkwiu { get; set; }

		[DisplayName("Jednostka miary")]
		[Required(ErrorMessage = "Measure unit required")]
		[StringLength(10, ErrorMessage = "Measure unit must be under 10 characters")]
		public string MeasureUnit { get; set; }

		[DisplayName("Cena netto")]
		[Required(ErrorMessage = "Net price required")]
		[Range(0, (double)decimal.MaxValue, ErrorMessage = "Net price must be greater than or equal to 0")]
		public decimal NetPrice { get; set; }

		[DisplayName("Stawka VAT")]
		[Required(ErrorMessage = "Vat rate required")]
		[StringLength(10, ErrorMessage = "Vat rate must be under 10 characters")]
		public string VatRate { get; set; }
	}
}

Według wspomnianego tutorialu, a także tego artykułu, dodanie atrybutów z przestrzeni nazw System.ComponentModel wystarcza do osiągnięcia automatycznej walidacji. Nie u mnie. Właściwość ModelState mojego kontrolera, zachowując pokerową twarz, niezmiennie spełnia warunek: StateModel.IsValid == true.

Próbowałem co najmniej kilku sposobów, jednak w dalszym ciągu nie osiągnąłem tego właściwego, który, jak to bywa w takich przypadkach, zapewne okaże się banalny. Jeśli w końcu znajdę źródło problemu i jego rozwiązanie, poświęcę temu kilka linijek kolejnego wpisu, w którym, mam nadzieję, zaprezentuję również pierwszą uruchamialną wersję programu. Zapraszam!