Skocz do zawartości

wiesniak@Blog.FA

  • wpisy
    12
  • komentarzy
    134
  • wyświetleń
    40067

Tworzymy stronę www w ASP.NET cz.1


wies.niak

4663 wyświetleń

Hejo!

Internet pełny jest kursów tworzenia stronek www, również w ASP.NET. Najczęściej jednak są to kursy prezentujące kolejne elementy technologii. Celem tego wpisu (oraz kolejnych części) nie jest nauka ASP.NET od podstaw. Mam zamiar przedstawić i omówić rozwiązania, których nauczyła mnie praca zawodowa. Zacznę od rzeczy podstawowych, od szkieletu aplikacji, a z czasem przejdę do rzeczy bardziej odrobinkę skomplikowanych, jednak bez obaw ? nie jest to seria wpisów dla hardkorów. Wystarczy podstawowa znajomość .NET, a wręcz ogólne wyczucie programowania.

(PS. Link do kodu na końcu)

Chyba każdy, kto zaczynał programowanie wie, że pierwotnie kod to chaos. Większość moich doświadczeń ze studiów to właśnie chaos ? napisać program aby działał, oddać, zaliczyć, zarchiwizować. W pracy takie podejście zwykle nie zadziała, bo w kodzie musi rozeznawać się reszta zespołu, a struktura aplikacji musi pozwalać na dość prostą rozbudowę.

Podzielimy aplikację na warstwy logiczne:

  • interfejs użytkownika
  • sterowanie aplikacją
  • dostęp do danych

Przedstawiony podział nazywa się wzorcem MVC - Model View Controller. Model to nasz dostęp do danych, widok (View) to interfejs użytkownika, a kontroler, zgodnie z nazwą, odpowiada za sterowanie. MVC jest wzorcem bardzo ogólnym ? można go spotkać pod różnymi postaciami, w różnych technologiach oraz językach. Jego idea jest bardzo prosta: kontroler steruje aplikacją ? pobiera dane z bazy, wyświetla je użytkownikowi oraz przetwarza akcje wykonane przez użytkownika. My skupimy się na ?wersji? zwanej MVP, która jest bardzo popularna wśród aplikacji ASP.NET. Litera ?P? pochodzi od słowa Presenter. Różnica między MVC, a MVP jest taka, że w MVP to widok odpowiedzialny jest za obsługę zdarzeń użytkownika ? wynika to bezpośrednio ze struktury ASP.NET.

blogentry-7427-1330303599.png

Źródło: Klik!

Trochę się nagadałem, więc może zacznijmy wizualizować tę paplaninę. A w ogóle co to będzie za program? Powiedzmy, że zrobimy stronkę o książkach ? książki, gatunki, autorzy. Niech nazywa się szumnie ?Księgarnia CDA?.

Stwórzmy nowy projekt ASP.NET Empty Web Application i, biorąc pod uwagę podział na warstwy, nazwijmy go ?KsiegarniaCDA.GUI?. Do naszej solucji dodajmy teraz nowy projekt typu Class Library i nadajmy mu nazwę ?KsiegarniaCDA.Presentation?. Następnie dodajmy trzeci projekt odpowiadający modelowi danych i nazwijmy go ?KsiegarniaCDA.DAL?. DAL to skrót od Data Access Layer, czyli warstwa dostępu do danych. Mamy już trzy projekty, ale dodamy jeszcze jeden, o nazwie ?KsiegarniaCDA.DTO? i również będzie to Class Library. DTO jest akronimem słów Data Transfer Object, a więc obiekt transportu danych. Mówiąc po ludzku, nasz tajemniczy projekt będzie zawierał klasy, w których będziemy przenosić dane pomiędzy warstwami aplikacji. Oczywiście można by było przekazywać pojedyncze informacje jako parametry wejściowe i wyjściowe metod, ale takie rozwiązanie jest skrajnie niewygodne.

Posiadamy już wszystkie projekty niezbędne do zbudowania aplikacji. Potrzebujemy jeszcze miejsca, w którym będziemy składować dane, a więc bazę danych. Aby nie komplikować za bardzo na początek, zamodelujemy bazę jako zbiór kolekcji w dodatkowym, piątym projekcie ?KsiegarniaCDA.DB?.

Ze wszystkich projektów usunąłem plik Class1.cs i nasza aplikacja prezentuje się następująco:

blogentry-7427-1330303837.png

Skoro mamy już strukturę projektu pora zastanowić się, jak w ogóle ma działać nasza aplikacja i co ma w niej być. Chcielibyśmy aby nasz program przechowywał informacje o książkach, autorach oraz o gatunkach książek. Może to być tak, jak na diagramie poniżej.

blogentry-7427-1330303843.png

Jedna książka może mieć tylko jednego autora i należeć do jednego gatunku. Jeden autor może być twórcą wielu książek, a jeden gatunek może zostać przypisany do wielu książek.

Przenieśmy nasz schemat do aplikacji. Najpierw stworzymy 3 klasy DTO: Ksiazka, Gatunek oraz Autor z właściwościami dokładnie takimi, jak na diagramie.

Ksiazka:

namespace KsiegarniaCDA.DTO
{
    using System;

    public class Ksiazka
    {
        public int Id { get; set; }
        public string Tytul { get; set; }
        public int IdAutora { get; set; }
        public int IdGatunku { get; set; }
        public DateTime? RokWydania { get; set; }
        public string Wydawnictwo { get; set; }
        public string Opis { get; set; }
    }
}

Gatunek:

namespace KsiegarniaCDA.DTO
{
    using System;

    public class Gatunek
    {
        public int Id { get; set; }
        public string Nazwa { get; set; }
        public string Opis { get; set; }
    }
}

Autor:

namespace KsiegarniaCDA.DTO
{
    using System;

    public class Autor
    {
        public int Id { get; set; }
        public string Imie { get; set; }
        public string Nazwisko { get; set; }
        public DateTime? DataUrodzenia { get; set; }
    }
}

Mamy już nasze klasy do przenoszenia danych, więc możemy zasymulować bazę. W projekcie DB tworzymy klasę DB, a w niej kolekcje odpowiadające naszym tabelkom. Ponieważ baza zazwyczaj jest jedną instancją, do której łączy się wiele instancji aplikacji, nasza klasa jak i tabelki będą statyczne.

Jednak zanim przystąpimy do tworzenia bazy, musimy zrobić coś jeszcze, a mianowicie musimy powiązać ze sobą projekty poprzez referencje. I tak, to DB dostęp może mieć tylko DAL. DAL może być dostępny tylko dla warstwy prezentacji (pod żadnym pozorem nie można korzystać z warstwy danych w warstwie GUI). Natomiast warstwa GUI musi być powiązana z warstwą prezentacji. Napisałem wcześniej, że warstwa prezentacji zajmuje się sterowaniem działaniem aplikacji, a co za tym idzie, musi komunikować się z GUI. Z drugiej strony obsługa zdarzeń ASP.NET musi znajdować się w code-behind, bo tak zostało to zaprojektowane, czyli GUI musi mieć możliwość kontaktu z warstwą prezentacji. Mamy więc problem referencji krzyżowej ? GUI potrzebuje dostępu do presenterów, a presentery do GUI. Oczywiście nie możemy dodać na krzyż referencji, bo solucja nam się po prostu nie zbuduje. Problem rozwiązuje się przekazując obiekt strony do presentera, a referencję warstwy prezentacji dodaje się do GUI. Szczegółowo opiszę to troszkę później i nawet jeśli teraz jest to niejasne, za parę akapitów się wyklaruje. Pozostaje nam projekt DTO, który, zgodnie z założeniem, nie referuje do żadnego projektu, ale wszystkie odnoszą się do niego. Pomijając biblioteki domyślnie dodane do projektów, nasza solucja wygląda tak:

blogentry-7427-1330304052.png

Zajmijmy się w końcu naszą bazą. Dodajemy trzy kolekcje Ksiazki, Autorzy oraz Gatunki. Ponieważ symulujemy bazę, stworzymy prywatną metodę dodającą po kilka obiektów do każdej z kolekcji. Metodę tę zawołamy ze statycznego konstruktora.

namespace KsiegarniaCDA.DB
{
    using System;
    using System.Collections.Generic;
    using KsiegarniaCDA.DTO;

    public static class DB
    {
        #region Properties

        public static List<Ksiazka> Ksiazki { get; private set; }

        public static List<Autor> Autorzy { get; private set; }

        public static List<Gatunek> Gatunki { get; private set; }

        #endregion

        #region Constructors
        
        static DB()
        {
            InitializeDB();
        }

        #endregion

        #region Private methods

        private static void InitializeDB()
        {
            Autorzy = new List<Autor>()
            {
                // Kilku autorów
            };

            Gatunki = new List<Gatunek>()
            {
                // Kilka gatunków
            };

            Ksiazki = new List<Ksiazka>()
            {
                // Kilka książek
            };
        }

        #endregion
    }
}

Jak widać, fragmenty kodu zostały otoczone regionami, których zadaniem jest grupowanie podobnych elementów, dzięki czemu zachowujemy pewną uporządkowaną strukturę.

Baza gotowa, więc pora wymyślić, co chcemy wyświetlać. Zaczniemy naszą zabawę od jednej strony, która pozwoli nam wyszukiwać książki po zadanych kryteriach.

Zabierając się za tworzenie stron, zwykle pomyśleć należy o wykorzystaniu mechanizmu Master Page?ów, stworzeniu odpowiednich klas bazowych czy zapewnieniu obsługi błędów. Ponieważ w tym wpisie pragnę skupić się na strukturze projektu, odpuszczę te wszystkie rzeczy. Przy odrobinie szczęścia zajmę się nimi w kolejnych wpisach.

Tworzymy w projekcie GUI katalog WebForms, w którym będziemy przechowywać stronki. Do katalogu dodajemy nowy WebForm i nazywamy go ?BooksSearchForm.aspx?. Do głównego katalogu projektu dodajmy jeszcze WebForm o nazwie Default.aspx. Jest to w ASP.NET strona domyślna (tak jak często index.html) i jej jedynym zadaniem będzie u nas wykonać przekierowanie do wyszukiwania książek.

public partial class Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        Response.Redirect("/WebForms/BooksSearchForm.aspx");
    }
}

Dodajmy teraz kryteria wyszukiwania. Nie przepadam za zabawą w stylowanie, więc będzie to wyglądało paskudnie:-) Książek będziemy szukać po gatunku, autorze, tytule, roku wydania, opisie oraz wydawnictwie. Prawdę mówiąc wydawnictwa również mogłem zrobić słownikiem tak, jak autorów czy gatunki, ale jakoś mi umknęło. Następnym razem się przerobi :-) Oto nasze kryteria:

<fieldset style="padding:10px 0px">
    <legend>Kryteria wyszukiwania</legend>
    <span style="margin: 0px 10px;">
        <asp:Label ID="lblGatunek" runat="server" Text="Gatunek" Width="90px" />
        <asp:DropDownList ID="drpGatunki" runat="server" Width="100px" />
    </span>
    <span style="margin: 0px 10px;">
        <asp:Label ID="lblAutor" runat="server" Text="Autor" Width="90px" />
        <asp:DropDownList ID="drpAutorzy" runat="server" Width="100px" />
    </span>
    <span style="margin: 0px 10px;">
        <asp:Label ID="lblTytul" runat="server" Text="Tytul" Width="90px" />
        <asp:TextBox ID="txtTytul" runat="server" Width="120px" />
    </span>
    <br />
    <br />
    <span style="margin: 0px 10px;">
        <asp:Label ID="lblRokWydania" runat="server" Text="Rok wydania" Width="90px" />
        <asp:TextBox ID="txtRokWydania" runat="server" Width="90px" />
    </span>
    <span style="margin: 0px 10px;">
        <asp:Label ID="lblWydawnictwo" runat="server" Text="Wydawnictwo" Width="90px" />
        <asp:TextBox ID="txtWydawnictwo" runat="server" Width="90px" />
    </span>
    <span style="margin: 0px 10px;">
        <asp:Label ID="lblOpis" runat="server" Text="Opis" Width="90px" />
        <asp:TextBox ID="txtOpis" runat="server" Width="120px" />
    </span>
</fieldset>

Jak widać, przyjąłem schemat nazywania kontrolek 3-literowy skrót kontrolki + nazwa opisowa. Pozwala to dość łatwo zorientować się, z jaką kontrolką mamy do czynienia. W ramach dygresji powiem, że konsultowałem jakiś czas temu projekcik formularza rekrutacyjnego. Na tym formularzu kandydat miał oceniać swoje umiejętności. Tych umiejętności nazbierało się kilkadziesiąt (różne języki programowania, technologie, frameworki itd.), a do każdej umiejętności dochodziło pole z miejscem na ocenę. Autor formularza ponazywał te pola ?Umiejetnosc1?, ?Umiejetnosc2? itd. Była to jedna z pierwszych rzeczy jaką poleciłem zmienić :-) Wracając do naszego nazewnictwa, spotkałem się z jeszcze jedną konwencją: nazwa opisowa + typ kontrolki. W takim wypadku zamiast txtOpis mielibyśmy OpisTextBox. Której używać? Kwestia gustu.

Pod kryteriami wrzućmy przycisk odpalający wyszukiwanie:

<asp:Button ID="btnSearch" runat="server" Text="Szukaj" />

Teraz pozostaje dodać nam grid z wynikami wyszukiwania. Przede wszystkim zdefiniujemy kolumny ręcznie, więc ustawiamy właściwość AutoGenerateColumns na false. Oto nasz grid:

<asp:GridView ID="grdResults" runat="server" AutoGenerateColumns="false">
    <Columns>
        <asp:BoundField HeaderText="Tytuł" ItemStyle-Width="120px" />
        <asp:BoundField HeaderText="Autor" ItemStyle-Width="120px" />
        <asp:BoundField HeaderText="Gatunek" ItemStyle-Width="120px" />
        <asp:BoundField HeaderText="Rok wydania" ItemStyle-Width="100px" />
    </Columns>
</asp:GridView>

Stronkę mamy już gotową (z punktu widzenia samego wyglądu), więc zajmijmy się clou tego wpisu, czyli MVP. Ponownie, odpuszczę sobie chwilowo klasy bazowe, bo nie są one niezbędne.

Jak wspominałem już wcześniej, strona musi mieć dostęp do warstwy prezentacji aby informować ją o akcjach użytkownika. Z drugiej strony warstwa prezentacji musi mieć dostęp do strony, aby pobrać z niej dane lub je do niej wysłać. Aby taki scenariusz był możliwy, każda strona posiada własny obiekt prezentera jako prywatne pole. Przy tworzeniu obiektu prezentera, jako parametr konstruktora przekazuje się obiekt strony. Sęk w tym, że w konstruktorze prezentera nie da się określić typu strony (z braku referencji do projektu GUI). Dlatego też każda strona jest powiązana z interfejsem, który implementuje. Interfejs zdefiniowany jest w tym samym projekcie co prezenter, więc problem przypisania znika.

Zaimplementujmy teraz nasz interfejs oraz klasę prezentera. Tworzymy katalogi ?Interfaces? oraz ?Presenters? po czym dodajemy do nich odpowiednio interfejs o nazwie IBooksSearchView oraz klasę prezentera o nazwie BooksSearchPresenter. Przyjmiemy zasadę nazywania interfejsów INazwaStronyView, podobnie jak prezenterów NazwaStronyPresenter. Nasz interfejs jest pusty, ale będziemy go uzupełniać w miarę rozwoju strony.

W utworzonym prezenterze dodajemy konstruktor, który jako parametr przyjmuje utworzony interfejs. Tworzymy też prywatne pole, w którym ten interfejs będziemy przechowywać. Odrobinę wbrew zaleceniom Microsoftu, nazwy prywatnych pól będę zaczynał od podkreślnika (MS zaleca zaczynanie małą literką i używanie this tam, gdzie może wystąpić konflikt nazw, np. z parametrami metod). Nasze pole nazwiemy po prostu ?_view?. Oczywiście potrzebne będzie dodanie namespace?u interfejsu aby był on widoczny w prezenterze.

#region Fields

private IBooksSearchView _view;

#endregion

#region Constructors

public BooksSearchPresenter(IBooksSearchView view)
{
    _view = view;
}

#endregion

Wróćmy do naszej strony i dodajmy dziedziczenie po interfejsie.

public partial class BooksSearchForm : Page, IBooksSearchView

Skoro stworzyliśmy już klasę prezentera, to dodajmy ją na naszej stronie. Tworzymy pole ?_presenter?, przeciążamy metodę OnInit i w niej tworzymy obiekt prezentera.

#region Fields

private BooksSearchPresenter _presenter = null;

#endregion

#region Protected methods

protected override void OnInit(EventArgs e)
{
    base.OnInit(e);
    _presenter = new BooksSearchPresenter(this);
}

#endregion

Mamy już dostęp do prezentera więc możemy zająć się ładowaniem danych. Nasza strona posiada dwie listy rozwijane (Gatunki, Autorzy), które musimy wypełnić. Dodajemy do prezentera metodę InitPage.

#region Public methods

public void InitPage()
{

}

#endregion

Chwilowo zostawmy ją pustą ? powiązaniem prezentera z DAL zajmiemy się później, a teraz skupimy się na relacjach między stroną a prezenterem. Naszą metodę wywołamy w zdarzeniu OnLoad strony, przy czym zrobimy to tylko przy jej otwarciu ? nie będziemy inicjalizować strony za każdym postbackiem, ponieważ nie ma to sensu, a mogło by być wręcz szkodliwe (np. czyszczenie jakiegoś pola, które uzupełnił użytkownik w trakcie pracy na stronie).

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);

    if (!IsPostBack)
    {
        _presenter.InitPage();
    }
}

Teraz zajmijmy się jedynym przyciskiem, jaki mamy na stronie. Dodajemy obsługę zdarzenia OnClick w markupie strony (w kontrolce przycisku oczywiście):

OnClick="btnSearch_Click"

po czym w code behind dodajemy odpowiednią metodę.

private void btnSearch_Click(object sender, EventArgs e)
{
            
}

W tym miejscu dzieje się istotna rzecz z punktu widzenia MVP. Jedyne co robimy w metodzie obsługi zdarzenia od użytkownika to zawołanie odpowiedniej metody z prezentera. Nic więcej. Przechodzimy więc do prezentera i dodajemy metodę Search, chwilowo pustą.

public void Search()
{

}

Metoda ta nie przyjmuje ani nie zwraca parametrów ? prezenter sam zadba o pobranie tego, co mu potrzeba oraz o załadowanie tego co trzeba. Pamiętajmy, to prezenter kontroluje sytuację, a nie strona, dlatego w kodzie strony nie dodajemy żadnej logiki działania aplikacji. Nasze zdarzenie kliknięcia wygląda jak poniżej i już nic w nim nie będziemy zmieniać:

private void btnSearch_Click(object sender, EventArgs e)
{
    _presenter.Search();
}

Oprogramujmy teraz wyszukiwanie. Będzie ono składać się z następujących czynności:

  • pobranie kryteriów wyszukiwania
  • wykonanie wyszukiwania poprzez odpowiednią metodę w DAL
  • opcjonalne przetworzenie wyników
  • załadowanie ich na stronę

Aby zrealizować powyższy scenariusz będziemy potrzebować nowych klas DTO aby przechować kryteria oraz wyniki wyszukiwania. Dodajemy więc klasę BooksSearchSearchCriteria

public class BooksSearchSearchCriteria
{
    public int? IdAutora { get; set; }
    public int? IdGatunku { get; set; }
    public string Tytul { get; set; }
    public string Opis { get; set; }
    public int? RokWydania { get; set; }
    public string Wydawnictwo { get; set; }
}

oraz BooksSearchSearchResult

public class BooksSearchSearchResult
{
    public int IdKsiazki { get; set; }
    public string Tytul { get; set; }
    public string Autor { get; set; }
    public string Gatunek { get; set; }
    public string RokWydania { get; set; }
}

Pobierzmy teraz kryteria wyszukiwania. W tym celu musimy dodać metodę do interfejsu oraz zaimplementować ją na stronie. Będziemy potrzebowali pobrać wartości z list rozwijanych, gdzie właściwość SelectedValue jest stringiem, podobnie jak wartość z pola txtRokWydania. Aby ułatwić sobie życie, zrobimy dwie rzeczy. Po pierwsze napiszemy dwie metody parsujące: string na nullowalny int oraz nullowalny int na string.

private int? ParseInt(string value)
{
    int parsedValue;
    if (!int.TryParse(value, out parsedValue))
    {
        return null;
    }
    return (int?)parsedValue;
}

private string ParseString(int? value)
{
    return value.HasValue ? value.Value.ToString() : string.Empty;
}

Po drugie dla każdego kryterium wyszukiwania, czyli każdej kontrolki z której pobieramy lub do której zapisujemy dane stworzymy właściwość, która nam to ułatwi. W tej chwili nie ma to jakiegoś wielkiego znaczenia, ponieważ do kryteriów odwołujemy się tylko raz, ale przy większym formularzu częste odwołania do kontrolek uzasadniają takie podejście.

private int? IdAutora
{
    get
    {
        return ParseInt(drpAutorzy.SelectedValue);
    }
    set
    {
        drpAutorzy.SelectedValue = ParseString(value);
    }
}

private int? IdGatunku
{
    get
    {
        return ParseInt(drpGatunki.SelectedValue);
    }
    set
    {
        drpGatunki.SelectedValue = ParseString(value);
    }
}

private int? RokWydania
{
    get
    {
        return ParseInt(txtRokWydania.Text);
    }
    set
    {
        txtRokWydania.Text = ParseString(value);
    }
}

private string Tytul
{
    get
    {
        return txtTytul.Text;
    }
    set
    {
        txtTytul.Text = value;
    }
}

private string Opis
{
    get
    {
        return txtOpis.Text;
    }
    set
    {
        txtOpis.Text = value;
    }
}

private string Wydawnictwo
{
    get
    {
        return txtWydawnictwo.Text;
    }
    set
    {
        txtWydawnictwo.Text = value;
    }
}

Przygotowaliśmy pomoce, więc wróćmy do metody. Nazwiemy ją GetSearchCriteria i będzie ona zwracać odpowiedni obiekt klasy z DTO:

public BooksSearchSearchCriteria GetSearchCriteria()
{
    BooksSearchSearchCriteria searchCriteria = new BooksSearchSearchCriteria();

    searchCriteria.IdAutora = IdAutora;
    searchCriteria.IdGatunku = IdGatunku;
    searchCriteria.Opis = Opis;
    searchCriteria.RokWydania = RokWydania;
    searchCriteria.Tytul = Tytul;
    searchCriteria.Wydawnictwo = Wydawnictwo;

    return searchCriteria;
}

Jak widać, dzięki zastosowaniu właściwości oraz parsowania w osobnych metodach kod jest zdecydowanie bardziej przejrzysty niż w sytuacji, gdyby całą konwersję zrobić w metodzie. Użycie pól tekstowych jest oczywiste, jednak można zastanawiać się, skąd w SelectedValue list znajdzie się Id autora lub gatunku. Jest to kwestia odpowiedniego powiązania danych z kontrolką i omówię to odrobinę później, gdy dojdziemy do wypełniania kontrolek.

Odpocznijmy chwilkę od strony i spójrzmy w kierunku bazy. Zaimplementujmy potrzebną nam klasę w warstwie dostępu do danych. W tym celu stwórzmy katalog DataManagers, a w nim klasę BooksSearchDataManager. Pisząc tę klasę zrobię paskudną rzecz, a mianowicie pominę obsługę błędów. Jest to karygodne, ale w tej chwili nam niepotrzebne. Poza tym chciałbym temu poświęcić osobny wpis. Dlatego na potrzeby naszego przykładu założymy, że zawsze wszystko zadziała (bo dlaczego nie, skoro mamy fikcyjną bazę).

Przede wszystkim potrzebujemy dwóch metod pobierających autorów oraz gatunki. Wartości tych potrzebujemy do wypełnienia list rozwijanych, więc tak naprawdę potrzebujemy list wartość-opis. Aby ograniczyć ilość pobieranych danych do niezbędnego minimum, wprowadźmy klasę Item, która będzie posiadała tylko dwie właściwości: Value oraz Text. Umieścimy ją oczywiście w projekcie DTO.

public class Item
{
    public object Value { get; set; }
    public string Text { get; set; }
}

Nasze dwie metody pobierające dane dla list będą miały taką postać:

public IEnumerable<Item> AutorzyComboPobierz()
{
    return DB.Autorzy.Select(a => new Item() { Value = a.Id, Text = a.Imie + " " + a.Nazwisko });
}

public IEnumerable<Item> GatunkiComboPobierz()
{
    return DB.Gatunki.Select(g => new Item() { Value = g.Id, Text = g.Nazwa });
}

Jak widać, użyłem tu metody Select z Linq to Entities, która przetwarza wszystkie obiekty z kolekcji w taki sposób, w jaki zostało to określone wyrażeniu podanym jako argument. Ja w tym miejscu użyłem wyrażeń lambda, które idealnie pasują do takich zastosowań, jednak równie dobrze mógłby tu być podany delegat lub zwykła metoda. Istotne jest, że użycie Select zwróci nam kolekcję obiektów typu Item.

Dorzućmy od razu metodę do wyszukiwania. Jako parametr wejściowy będzie ona przyjmować nasze kryteria wyszukiwania, a zwracać kolekcję obiektów klasy z wynikami. Do dzieła!

public IEnumerable<BooksSearchSearchResult> SearchBooks(BooksSearchSearchCriteria searchCriteria)
{
    IEnumerable<Ksiazka> results = DB.Ksiazki;

    if (searchCriteria.IdAutora.HasValue)
    {
        results = results.Where(r => r.IdAutora == searchCriteria.IdAutora.Value);
    }
    if (searchCriteria.IdGatunku.HasValue)
    {
        results = results.Where(r => r.IdGatunku == searchCriteria.IdGatunku.Value);
    }
    if (!string.IsNullOrEmpty(searchCriteria.Opis))
    {
        results = results.Where(r => r.Opis.Contains(searchCriteria.Opis));
    }
    if (!string.IsNullOrEmpty(searchCriteria.Tytul))
    {
        results = results.Where(r => r.Opis.Contains(searchCriteria.Tytul));
    }
    if (!string.IsNullOrEmpty(searchCriteria.Wydawnictwo))
    {
        results = results.Where(r => r.Opis.Contains(searchCriteria.Wydawnictwo));
    }
    if (searchCriteria.RokWydania.HasValue)
    {
        results = results.Where(r => r.RokWydania.HasValue && r.RokWydania.Value.Year == searchCriteria.RokWydania.Value);
    }

    var returnResults = from result in results
                        from autor in DB.Autorzy.Where(a => a.Id == result.IdAutora).DefaultIfEmpty()
                        from gatunek in DB.Gatunki.Where(g => g.Id == result.IdGatunku).DefaultIfEmpty()
                        select new BooksSearchSearchResult()
                        {
                            IdKsiazki = result.Id,
                            RokWydania = result.RokWydania.HasValue ? result.RokWydania.Value.Year.ToString() : string.Empty,
                            Tytul = result.Tytul,
                            Autor = autor != null ? autor.Imie + " " + autor.Nazwisko : string.Empty,
                            Gatunek = gatunek != null ? gatunek.Nazwa : string.Empty,
                        };


    return returnResults;
}

Zaczynamy od założenia, że przy braku kryteriów zwracamy wszystkie książki, po czym budujemy zapytanie linq dla kolejnych kryteriów. Warto zauważyć, że kolejne użycia metody Where nie przeliczają na nowo kolekcji, a jedynie dokładają cegiełkę do zapytania, jakie ma się wykonać na kolekcji Ksiazki. Widać to w debuggerze ? gdy spróbujemy podejrzeć kolekcję, zobaczymy informację ?Expanding the Results View will enumerate the IEnumerable?. To samo tyczy się zapytania przy końcu metody. Wyniki zapytania otrzymamy dopiero, gdy tego zarządamy np. za pomocą metody ToList(), ToArray() lub poprzez enumerację kolekcji. Jest to tak zwany Lazy Loading, a można o nim poczytać np. tu: klik.

Przyjrzyjmy się jeszcze na chwilę temu końcowemu zapytaniu. Pobierając informację o książkach muszę zdobyć informacje o autorach oraz gatunkach, dlatego muszę wykonać połączenie ?tabel? (naszych kolekcji). Osoby znające SQL mogą z góry wyczuć pułapkę takiego połączenia. Mianowicie jeśli zrobię zwykłe połączenie (inner join), to dostanę iloczyn kartezjański wszystkich trzech tabel. Pułapka polega na tym, że jeśli dla danej książki nie zostanie znaleziony autor lub gatunek, to taka książka wyleci z wyniku końcowego. Dlatego też zamiast zwykłego połączenia ?dokładamy? do książek informację o autorach i gatunkach (wykonujemy left outer join), lecz w wypadku nieznalezienia danej informacji, zostanie dołożony null, a książka pozostanie w kolekcji.

Jako bonus użyłem dwóch form zapytania linq ? metod oraz formy SQL-podobnej. Gdybyśmy chcieli zbudować ostatnie zapytanie w całości z formy SQL-podobnej, mogło by to wyglądać tak:

var returnResults = from r in results
                    join a in DB.Autorzy on r.IdAutora equals a.Id into resultsAutorzy
                    from ra in resultsAutorzy.DefaultIfEmpty()
                    join g in DB.Gatunki on r.IdGatunku equals g.Id into resultsGatunki
                    from rg in resultsGatunki.DefaultIfEmpty()
                    select new BooksSearchSearchResult()
                    {
                        IdKsiazki = r.Id,
                        RokWydania = r.RokWydania.HasValue ? r.RokWydania.Value.Year.ToString() : string.Empty,
                        Tytul = r.Tytul,
                        Autor = ra != null ? ra.Imie + " " + ra.Nazwisko : string.Empty,
                        Gatunek = rg != null ? rg.Nazwa : string.Empty,
                    };

Gdyby ktoś miał ochotę przetestować działanie left outer join, proponuję zakomentować jakiś gatunek oraz autora i podmienić kod zwracający wyniki na

var returnResults = from r in results
                    join a in DB.Autorzy on r.IdAutora equals a.Id
                    join g in DB.Gatunki on r.IdGatunku equals g.Id
                    select new BooksSearchSearchResult()
                    {
                        IdKsiazki = r.Id,
                        RokWydania = r.RokWydania.HasValue ? r.RokWydania.Value.Year.ToString() : string.Empty,
                        Tytul = r.Tytul,
                        Autor = a != null ? a.Imie + " " + a.Nazwisko : string.Empty,
                        Gatunek = g != null ? g.Nazwa : string.Empty,
                    };

Przy braku kryteriów wyszukiwania, kolekcja wynikowa powinna być mniejsza od tej na bazie.

Skoro zaimplementowaliśmy już DAL, możemy wrócić do prezentera. Zaczniemy od uzupełnienia metody InitPage. Zanim jednak to zrobimy, dodamy nasz data manager jako prywatne pole prezentera.

private BooksSearchDataManager _booksSearchDataManager = null;

Potrzebne nam też będą dwie metody do wypełniania list, więc dodajmy je (chwilowo puste) do kodów strony

public void LoadComboAutorzy(IEnumerable<Item> data)
{

}

public void LoadComboGatunki(IEnumerable<Item> data)
{

}

public void ShowSearchResults(IEnumerable<BooksSearchSearchResult> searchResults)
{

}

oraz w interfejsu.

void LoadComboAutorzy(IEnumerable<Item> data);

void LoadComboGatunki(IEnumerable<Item> data);

void ShowSearchResults(IEnumerable<BooksSearchSearchResult> searchResults);

Korzystając z okazji dodałem od razu metodę ShowSearchResults, która będzie wyświetlać nasze wyszukane dane.

Przejdźmy do InitPage. Pobieramy w niej autorów oraz gatunki, wykonujemy dodatkowe sortowanie po tekście i dołączamy na początek kolekcji pusty element aby można było szukać bez tych kryteriów. Ponieważ operujemy na IEnumerable<>, a nie chciałem przechodzić na listy, użyłem metody Concat, która skleja kolekcje nie zaburzając ich porządku.

public void InitPage()
{
    var autorzy = _booksSearchDataManager.AutorzyComboPobierz();
    autorzy = (new Item[] { new Item() { Value = string.Empty, Text = string.Empty } }).Concat(autorzy.OrderBy(a => a.Text));
    _view.LoadComboAutorzy(autorzy);

    var gatunki = _booksSearchDataManager.GatunkiComboPobierz();
    gatunki = (new Item[] { new Item() { Value = string.Empty, Text = string.Empty } }).Concat(gatunki.OrderBy(g => g.Text));
    _view.LoadComboGatunki(gatunki);
}

Idąc za ciosem uzupełnijmy metodę Search.

public void Search()

{
    BooksSearchSearchCriteria searchCriteria = _view.GetSearchCriteria();

    IEnumerable<BooksSearchSearchResult> searchResults = _booksSearchDataManager.SearchBooks(searchCriteria);
            
    _view.ShowSearchResults(searchResults);
}

Zgodnie ze scenariuszem, pobieramy kryteria, wyszukujemy i ładujemy wyniki na stronkę.

Oto gotowy prezenter. Pozostały nam do uzupełnienia trzy metody w code behind, więc się nimi zajmijmy. Zaczniemy od podobnych metod ładujących listy:

public void LoadComboAutorzy(IEnumerable<Item> data)
{
    drpAutorzy.DataSource = data;
    drpAutorzy.DataBind();
}

public void LoadComboGatunki(IEnumerable<Item> data)
{
    drpGatunki.DataSource = data;
    drpGatunki.DataBind();
}

Przypisujemy dane z prezentera do właściwości DataSource list, a następnie wykonujemy powiązanie danych z kontrolką. Tajemnica naszego sukcesu w markupie. Do każdej z kontrolek dodajemy dwie właściwości

DataTextField="Text" DataValueField="Value"

Określają one, jakie właściwości z obiektów mają być wzięte jako opis (DataTextField) oraz wartość (DataValueField). Dzięki temu będziemy widzieć np. Andrzej Sapkowski, a SelectedValue zwróci nam wartość 2, podobnie z gatunkami.

Podobną sztuczkę musimy wykonać z gridem. Uzupełniamy więc metodę ShowSearchResults

public void ShowSearchResults(IEnumerable<BooksSearchSearchResult> searchResults)
{
    grdResults.DataSource = searchResults;
    grdResults.DataBind();
}

Oraz uzupełniamy właściwości DataField kolumn w gridzie

<asp:BoundField HeaderText="Tytuł" ItemStyle-Width="120px" DataField="Tytul" />
<asp:BoundField HeaderText="Autor" ItemStyle-Width="120px" DataField="Autor" />
<asp:BoundField HeaderText="Gatunek" ItemStyle-Width="120px" DataField="Gatunek" />
<asp:BoundField HeaderText="Rok wydania" ItemStyle-Width="100px" DataField="RokWydania" />

Pora na odpalenie naszej aplikacji. Zbudowało się i? wysyp :-) Nie może znaleźć metody btnSearch_Click. No tak, metoda powinna być protected, a nie private. Poprawiamy i puff, działa!

blogentry-7427-1330305128.png

I to tyle w tej części. W następnym wpisie rozbudujemy troszkę naszą aplikację, dodając jej coś przydatnego. A tym czasem zapraszam do komentowania oraz zadawania pytań. Działający kod jest tu: KsiegarniaCDA___cz1.zip

Do przeczytania!

13 komentarzy


Rekomendowane komentarze

Hmm. Choć na razie to dla mnie prawie całkowicie niezrozumiałe to inicjatywa podoba mi się jak najbardziej. Może za pół roku będę bardziej w tych tematach obyty, że kod nie sprawi mi problemu. Czekam na więcej :) .

Link do komentarza

Jakoś mam awersję do webowego .NET. O ile jeszcze do WebService'ów jest to całkiem wygodne i proste, o tyle stosowanie C# w typowym tworzeniu stron odrzuca mnie niemiłosiernie. Podobnie zresztą jest z JSP. Przekonany w tym temacie jestem bardziej do pythonowego Django, ew. php'owy Cake czy kohana.

BTW Dodając do tabeli z książkami numer isbn, można, zamiast IDksiazki, użyć go jako klucza głównego i przy okazji rozszerzyć możliwości wyszukiwania. Sprawa na pozór mało znacząca, ale nie rzadko popełnia się podobny błąd olewając całkowicie kwestie normalizacyjne. Sporo oznaczeń jest ustandaryzowanych i opatrzonych w mechanizmy zabezpieczające, typu cyfry kontrolne i należy z tego w miarę możliwości korzystać.

Wiadomo, że tutaj mamy tylko tak dla przykładu, małą aplikację, ale tego typu pierdoły mogą stwarzać czasem problemy, jak się komuś guziczki pomieszają, a przy ew. perspektywach rozszerzania aplikacji, czy też integracji z innym oprogramowaniem, może to stanowić duzy kłopot. Zawsze lepiej być tym, który się do standardów dokopał i je zastosował u siebie, niż stroną która dodała kolejne pole ID i w sumie jest winna ewentualnym problemom.

Link do komentarza

Ja właśnie lubię ASP.NETowe tworzenie stron ponieważ pozwala ono zachować porządek w kodzie. Wszystko ma swoje miejsce i wszystko się ładnie układa. PHP kojarzy mi się niezmiennie z chaosem. Nie miałem z nim za wiele do czynienia, ale mnie odrzuca. Być może faktycznie robi się to bardziej przejrzyste z użyciem jakiegoś frameworka (kohana -> mvc :-) ).

ISBN w zasadzie zignorowałem aby sobie nie utrudniać - łatwiej wpisywać kolejne numerki :-) Oczywiście w praktycznym zastosowaniu miało by to rację bytu. Akurat nie upatrywałbym jakichś problemów w użyciu numerków zamiast ISBN jako klucza - baza sama w sobie będzie spójna, a ISBN zawsze może być kolejną kolumną z dodanym indeksem.

Wpisy moje mają raczej skupiać się na architekturze aplikacji, dlatego formaty danych raczej upraszczam. Tak jak i parę innych rzeczy :-)

Nawiasem mówiąc chętnie poczytałbym podobny wpis dotyczący np PHP czy Pythona.

Link do komentarza

No w sumie zgodzę się po części, bo najgorszy kod jaki w zyciu widziałem był właśnie w PHP i klepania w czystym PHP, też nie jestem fanem. Niemniej w frameworku zaczyna mieć to ręce i nogi. Kilka razy się przymierzałem do zapoznania się z Symfony, bo wygląda to na całkiem konkretny i uzyteczny kombajn. No, ale za każdym razem kończyło sie na kohanie, bo czasu nie miałem, zeby to wszystko rozgryzać :)

Podobny wpis w klimatach php czy pythona też bym chętnie zobaczył. No, ale na pewno na moim blogu się nie ukaże - nie mogę zawyżać swojego trollowego poziomu :P Chociaż z drugiej strony kusi, żeby pokazać o ile mniej byłoby roboty z tym w języku skryptowym :D Może by tak projekt "Księgarnia CDA" we wszystkich jezykach świata :D ?

Link do komentarza

Skuś się, skuś :) Jeśli Cię to zmotywuje, za jakiś czas mogę wypuścić wersję w Silverlight albo WPF - są podobne i z oboma mam parę miesięcy doświadczeń, którymi mogę się podzielić.

A co do tego "o ile mniej byłoby roboty w języku skryptowym" to nie wiem, czy możesz mnie przebić jeśli użyjesz jakiegoś frameworka (ten kod też się liczy, a co!) :P Z drugiej strony w WPF jest bardzo fajna biblioteczka MVVM Light, która ułatwia parę rzeczy i warto jej używać.

Jeśli się ktoś skusi na C++ i jakieś Qt / WxWidgets / GTK / cokolwiek, to chętnie zobaczę alternatywę :)

Link do komentarza

No w Silverlight chętnie by zobaczył jak się działa. Słyszałem o tym zarówno sporo dobrego jak i nie mniej hejterowskich opinii, ale ponoć wynalazek wart uwagi. Zresztą ktoś mi mówił, ze to nawet zaczęło m.in. pythona wspierać, do którego żywię wielkie uczucie :) A że nie znam się na tym zupełnie (Silverlight) to jakieś step by step dla noobów zawsze mile widziane :D

Link do komentarza

Mówisz i masz. W najbliższym czasie wrzucę wpis z Księgarnią CDA w SL :-) Generalnie SL to fajna technologia, choć czasami potrafi doprowadzić do szewskiej pasji - głównie dlatego, że w WPF da się coś zrobić, co w SL zostało obcięte. W mięczyczasie możesz pooglądać projekt z mojego poprzedniego wpisu - aplikacja jest w WPF zrobiona, więc ma wiele podobieństw do Silverlighta. W samym XAMLu można niezłe magie zdziałać, ale opanowanie tego wymaga czasu i chęci. Jak wiele rzeczy, SL jest easy to learn, hard to master.

Link do komentarza
Gość
Dodaj komentarz...

×   Wklejony jako tekst z formatowaniem.   Wklej jako zwykły tekst

  Maksymalna ilość emotikon wynosi 75.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • Utwórz nowe...