Historia faktur

Dzisiaj przedstawię niektóre elementy strony historii faktur. Słowo „niektóre” pojawia się dlatego, że strona ta czerpie garściami z innych – zaimplementowanych już i opisanych. Od razu pokażę jej wygląd, aby Czytelnik, czytając o kolejnych elementach, znał już ich umiejscowienie i przeznaczenie:

Filtrowanie
Filtrowanie danych omówiłem dość szczegółowo w tym wpisie. Wyjaśnię więc tylko, że faktury można filtrować po statusie, kliencie, produktach, dacie wystawienia i wartości. Na wyjaśnienie zasługuje filtrowanie produktów. Polega ono na tym, że użytkownik podaje początki nazw produktów, które powinna zawierać faktura, oddzielając je przecinkami. Oprócz podanych przez użytkownika, faktura może zawierać też inne produkty (czego nie widać na pokazanym screenie).

Status faktury
Możliwe statusy faktury to wystawiona i zapłacona. Pierwszy jest nadawany automatycznie po utworzeniu, drugi ustawia użytkownik (za pomocą przycisku zapłacona widocznego przy fakturze). Przy implementowaniu tej funkcjonalności, napotkałem problem natury językowej. Chciałem, aby w kodzie programu i bazie danych statusy miały nazwy angielskie (created, paid). Użytkownik powinien jednak widzieć je po polsku, dlatego napisałem dwie proste metody:

private static string TranslateInvoiceStatus(string status)
{
	if (status == Invoice.InvoiceStatus.Created.ToString())
		return "Wystawiona";
	else if (status == Invoice.InvoiceStatus.Paid.ToString())
		return "Zapłacona";
	else
		return "";
}

public static List<KeyValuePair<string, string>> GetInvoiceStatusList(this HtmlHelper helper) // ta metoda jest rozszerzeniem klasy HtmlHelper
{
	List<KeyValuePair<string, string>> result = new List<KeyValuePair<string, string>>();
	
	result.Add(new KeyValuePair<string, string>(TranslateInvoiceStatus(Invoice.InvoiceStatus.Created.ToString()), Invoice.InvoiceStatus.Created.ToString()));
	result.Add(new KeyValuePair<string, string>(TranslateInvoiceStatus(Invoice.InvoiceStatus.Paid.ToString()), Invoice.InvoiceStatus.Paid.ToString()));

	return result;
}

Przeznaczenie pierwszej metody jest uniwersalne, drugiej natomiast używam do wypełnienia rozwijanej listy statusów w filtrze faktur.

Termin płatności
Tutaj jedynie krótkie wyjaśnienie: jeśli faktura ma status inny niż zapłacona i jej termin płatności minął, jest on wyświetlany na czerwono – co widać na screenie.

PDF
Czas na najważniejszą funkcję całej aplikacji – generowanie pliku PDF. Po naciśnięciu przycisku PDF, widocznego przy każdej fakturze, użytownik wybiera, czy chce wygenerować oryginał dokumentu, czy jego kopię:

Po dokonaniu wyboru, wywoływana jest następująca metoda:

public ActionResult ToPdf(int id, PdfCreator.InvoiceType invoiceType)
{
	Invoice invoice = _invoiceRepository.GetById(id); // _invoiceRepository - prywatne repozytorium faktur
	string path = Server.MapPath("~/bin/lastInvoice.pdf"); // po stronie serwera, faktura zapisywana jest do pliku lastInvoice.pdf (być może nie jest to ostateczne rozwiązanie)
	string fileName = invoice.CreationDate.Value.ToShortDateString() + "_" + invoice.Customer.CompanyName + ".pdf"; // nazwa pliku otrzymywanego przez użytkownika jest postaci dataWystawienia_nazwaFirmyKlienckiej.pdf

	PdfCreator pdfCreator = new PdfCreator(invoice); // klasa PdfCreator tworzy dokument PDF na podstawie encji faktury z bazy danych
	pdfCreator.Create(invoiceType);
	pdfCreator.SaveDocument(path, false);

	return File(path, "PDF|*.pdf", fileName); // zwrócenie pliku PDF
}

Etykiety
Na koniec funkcjonalność niewidoczna na pokazanym na początku wpisu screenie – etykiety, pojawiające się po wskazaniu kursorem niektórych komórek tabeli zawierającej listę faktur:

  • po wskazaniu daty wystawienia faktury, wyświetlana jest etykieta zawierająca także datę sprzedaży,
  • do każdego produktu przypisana jest etykieta o treści: [ilość] [j.m.] ([wartość brutto] [waluta]), na przykład: 10 kg (100 PLN),
  • etykieta pola Do zapłaty zawiera również wartości pól Zapłacono i Pozostało do zapłaty.

Wygląda na to, że opisałem już wszystkie najważniejsze podstrony aplikacji. Prawdopodobnie zrezygnuję z tworzenia strony głównej, której rolę być może przejmie przedstawiona właśnie Historia faktur. Od teraz praca nad projektem będzie opierać się na testowaniu i szlifowaniu drobnych modułów programu, uzupełnię także informacje na stronie About. Jak widać, piętnaście tygodni to optymalna ilość czasu na stworzenie podobnego projektu i zdobycie sporej ilości wiedzy i doświadczenia.

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!