Smart UI ASP.NET Web Forms i Visual Studio łatwo wprowadzają początkujących programistów w świat technologii webowych . Ktoś mógłby powiedzieć, że pisanie aplikacji w ASP.NET sprowadza się do przyciągania i upuszczania kontrolek do okna designer-a HTML. W ASP.NET każda strona HTML o rozszerzeniu pliku .aspx zawiera swój drugi plik z kodem pobocznym. W tym kodzie zawiera obsługę zdarzeń, dostęp do danych i logikę biznesową aplikacji.

Dla początkującego programisty, te proste założenia znacznie przyspieszają naukę frameworka. Nie zmienia to jednak faktu, że ten domyślny styl programowania jest wadliwy i już dla średnio zawansowanych aplikacji webowych tworzy następujące, poważne problemy.


  • Powielania logiki biznesowej – logika jest ściśle powiązana z danym widokiem.
  • Brak możliwości testowania aplikacji.
  • Zapytania do baz danych są zawarte i ściśle powiązane z danym widokiem.

Inteligentne aplikacje UI - za wszelką cenę należy ich unikać, są one idealne do tworzenia prototypów, czy krótkotrwałych zastosowań.

Problem oczywiście pojawia się, gdy tymczasowa aplikacja musi być często modyfikowana i rozbudowywana. Aplikacja napisana tym antywzorcem, będzie trudna do utrzymania i w pewnym momencie krytycznym, będzie musiała być napisane od nowa.

Aby udowodnić, że wszystkie te twierdzenia są prawdziwe postanowiłem utworzyć prosty przykład aplikacji, napisanej w antywzorcu „Smart UI”. Dodam do niej trochę logiki biznesowej, tak, aby można było zobaczyć, jak zależności i odpowiedzialność są splecione ze sobą nieodwołalnie.

Studium przypadku tego antywzorca będzie prosta aplikacja e-commerce. Aplikacja będzie wyświetlać z bazy danych informacje o produktach do sprzedania. Aplikacja będzie zawierać tylko jedną stronę . Strona ta będzie prezentować listę produktów. Wyświetlę ich nazwę, sugerowaną, cenę detaliczną, cenę sprzedaży, zniżkę oraz stopę procentową oszczędności.

Ostatnie dwie informacje nie będą zawarte w bazie i trzeba będzie je obliczyć w kodzie. Dla specjalnych klientów sklep oferuje 5% zniżkę. Strona ma zawierać listę rozwianą z dwiema opcjami „Ceny z rabatem” i „ceny bez rabatu” . Po wybraniu tych opcji będzie można obejrzeć ceny z rabatem i bez.

Ten antywzorzec jest popularny wśród początkujących programistów, ponieważ wiele poradników on-line go prezentuję. Jest on także przykładem jaki łatwym i przyjaznym programistyczny narzędziem jest Visual Studio, ponieważ użycie tego wzorca sprowadza się do upuszczania kontrolek. Jest to też argument, ponieważ na prezentacjach technologicznych zawsze fajnie się prezentuje aplikację, którą się wyklikałem przez 5 minut. Tę tezę też zaraz udowodnię.

Oto krótki opis, jak łatwo utworzyć taką aplikację.

Na początku utworzyłem pusty projekt aplikacji ASP.NET. Oprócz pliku konfiguracyjnego nic więcej w nim się nie znajduję.

Tworzymy aplikację ASP.NET wzorcem Smart UI

Na początku utworzyłem pusty projekt aplikacji ASP.NET. Oprócz pliku konfiguracyjnego nic więcej w nim się nie znajduję.

clip_image001[6]

Aplikacja ta będzie potrzebować bazy danych, dlatego wywołuje menu kontekstowe projektu i z niego wybieram polecenie „Add -> New Item”. Później z następnego okna wybieram plik bazodanowy SQL Server.

image

Visual Studio pyta, czy chcę umieścić ten plik w folderze „App_Data” . Jest to specjalny wyodrębniony folder dla plików z danymi w ASP.NET. Klikam na „Tak”.

clip_image004[6]

W ten właśnie sposób utworzyłem pustą bazę danych, bez żadnej znajomości środowiska SQL Server. Jak można się domyślać, równie łatwo w Visual Studio mogę stworzyć tabelę do tej pustej bazy danych. Klikając dwa razy na plik „Sklep.mdf” w zakładce Solution Explorer wywołuję kolejną zakładkę „Server Explorer”. Zakładka ta wyświetla wszystkie połączenia do systemów bazodanowych, jak i ich plików baz. Rozszerzam ikonkę swojej bazy „Sklep.mdf” i na folderze „Tables” z menu kontekstowego wybieram polecenie „Add New Table”.

clip_image005[6]

Z kreatora nowej tabelki ustalam odpowiednie kolumny i ich wartości. Jak widać na poniższej ilustracji środowisko Visual Studio nawet wyświetla zapytanie SQL, na podstawie tego, co wyklikałem w tym kreatorze.

clip_image006[6]

Przed zapisem tabelki, ustawiam jeszcze we właściwościach kolumny „ProduktId” automatyczną inkrementację. Czyli, przy każdym nowym rekordzie klucz główny będzie automatycznie dodawany i zwiększany o jeden. Mamy w ten sposób pewność, że jest on unikatowy.

clip_image007[6]

Na koniec w zakładce skryptu „T-SQL” zmieniam nazwę tabeli z „Table” na „Products”.

Zapisuje definicję tabeli za pomocą przycisku „Update”, który jest widoczny na ilustracji 1.2 . W następnym oknie dialogowym klikam na przycisk „Update Database”.

clip_image008[6]

Mamy już tabelę, ale jest ona pusta, aby ją uzupełnić z zakładki „Serwer Explorer” wywołujemy menu kontekstowe na ikonce mojej, dopiero co utworzonej tabeli „Products”. Z menu wybieram polecenie „Show Table Data”.

clip_image009[6]

W nowej utworzonej zakładce uzupełniam danymi swoją tabele „Products”. Jeśli kolumny mają właściwe wartości to ich zawartość jest automatycznie dodawana do tabeli.

clip_image010[6]

Teraz, gdy mamy już tabelkę z danymi, przejdźmy do głównej części prezentacji problemu antywzorca SmartUI. Analogicznie do dodawania bazy SQL Server, teraz dodaję plik strony ASP.NET o rozszerzeniu „.aspx”.

smartUIAddDrop[5]

Napisanie całej logiki generującej tabele HTML oraz meta informacji o połączeniu do baz danej na pewno zajęłoby dużo czasu. Jednak magia tego wzorca polega na jego prostocie. Mogę zaznaczyć swoją tabelę z okna „Server Explorer” i upuść ją na okno grafiki swojej strony ASP.NET , wtedy cały kod zostanie utworzony za mnie.

Oto rezultat upuszczenia tabelki na widok strony.

clip_image011[6]

Poniżej znajduje się kod wygenerowany przez Visual Studio.

. Mamy tutaj kontrolkę „GridView”, która jest powiązana z inną kontrolką „SqlDataSource”. Największy problem antywzorca SmartUI tworzy kontrolka „SqlDataSource”.

<asp:SqlDataSource ID="SqlDataSource1" runat="server" 
    ConnectionString="<%$ ConnectionStrings:SklepConnectionString1 %>" 
    DeleteCommand="DELETE FROM [Products] WHERE [ProductId] = @ProductId" 
    InsertCommand="INSERT INTO [Products] ([ProductName], [RecommendedRetailPrice],
     [Selling Price]) VALUES (@ProductName, @RecommendedRetailPrice, @Selling_Price)" 
    ProviderName="<%$ ConnectionStrings:SklepConnectionString1.ProviderName %>" 
    SelectCommand="SELECT [ProductId], [ProductName], [RecommendedRetailPrice],
     [Selling Price] AS Selling_Price FROM [Products]"
    UpdateCommand="UPDATE [Products] SET [ProductName] = @ProductName, 
    [RecommendedRetailPrice] = @RecommendedRetailPrice,
     [Selling Price] = @Selling_Price WHERE [ProductId] = @ProductId">
    <DeleteParameters>
        <asp:Parameter Name="ProductId" Type="Int32" />
    </DeleteParameters>
    <InsertParameters>
        <asp:Parameter Name="ProductName" Type="String" />
        <asp:Parameter Name="RecommendedRetailPrice" Type="Decimal" />
        <asp:Parameter Name="Selling_Price" Type="Decimal" />
    </InsertParameters>
    <UpdateParameters>
        <asp:Parameter Name="ProductName" Type="String" />
        <asp:Parameter Name="RecommendedRetailPrice" Type="Decimal" />
        <asp:Parameter Name="Selling_Price" Type="Decimal" />
        <asp:Parameter Name="ProductId" Type="Int32" />
    </UpdateParameters>
</asp:SqlDataSource>

Kontrolka ta, ma w swoich właściwościach zadeklarowane zapytania SQL do baz danych, które będą wywoływać polecenia CRUD. Polecenia te są w warstwie widoku i nie mogą być w żaden sposób użyte ponownie, w innym pliku strony. Zdecydowanie mieszamy tutaj logikę warstwy prezentacji z warstwą dostępu do danych. Zapytania SQL powinny być przechowywane w bazach danych, a nie w widokach aplikacji.

Ten kod znacznikowy po stronie widoku wyświetli 4 kolumny, ale obiecałem także wyświetlenie informacji o stopie procentowej oszczędności, jak i o zniżce. Same wartości muszą być także wyświetlone w złotówkach. Przechodzimy więc do problemu braku warstwy logiki biznesowej.

protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
{
    if (e.Row.RowType == DataControlRowType.DataRow)
    {
        decimal RRP = decimal.Parse
            (((System.Data.DataRowView)e.Row.DataItem)
            ["RecommendedRetailPrice"].ToString());
        decimal SellingPrice = decimal.Parse
            (((System.Data.DataRowView)e.Row.DataItem)
            ["Selling_Price"].ToString());

        Label lblSellingPrice = (Label)e.Row.FindControl("lblSellingPrice");
        Label lblSavings = (Label)e.Row.FindControl("lblSavings");
        Label lblDiscount = (Label)e.Row.FindControl("lblDiscount");

        lblSavings.Text = DisplaySavings(RRP, ApplyExtraDiscountsTo(SellingPrice));
        lblDiscount.Text = DisplayDiscount(RRP, ApplyExtraDiscountsTo(SellingPrice));
        lblSellingPrice.Text = String.Format
            ("{0:C}", ApplyExtraDiscountsTo(SellingPrice));
    }
}

protected string DisplayDiscount(decimal RRP, decimal SalePrice)
{
    string discountText = "";

    if (RRP > SalePrice)
        discountText = String.Format("{0:C}", (RRP - SalePrice));

    return discountText;
}

protected string DisplaySavings(decimal RRP, decimal SalePrice)
{
    string savingsTest = "";

    if (RRP > SalePrice)
        savingsTest = (1 - (SalePrice / RRP)).ToString("#%");

    return savingsTest;
}

protected decimal ApplyExtraDiscountsTo(decimal OriginalSalePrice)
{
    decimal price = OriginalSalePrice;

    int discountType = Int16.Parse(this.ddlDiscountType.SelectedValue);

    if (discountType == 1)
    {
        price = price * 0.95M;
    }

    return price;
}

protected void ddlDiscountType_SelectedIndexChanged(object sender, EventArgs e)
{
    GridView1.DataBind();
}

Ten kod w C# w kodzie pobocznym uzupełni nowe kolumny i nada im wartości na bazie informacji z innych kolumn. Cały ten kod jest na sztywno powiązany z tą kontrolką, w tym widoku. Wynika to z modelu zdarzeniowego. Kod zdarzenia „GridView_RowDataBound” jest unikatowy dla tej strony i nie może być użyty ponownie na innej stronie. Stosując ten antywzorzec, aby uzyskać podobne rezultaty na innej stronie, musielibyśmy skopiować i wkleić ten sam kod, a to jest niezgodne z zasadą projektową DRY.

Oto ostateczny wynik.

SmarUI

Można by powiedzieć, że nie ma niczego złego w tym wzorcu, gdyby proces tworzenia aplikacji na tym się skończył. Jednak co, jeśli ta pojedyncza strona jest tylko częścią większej całości. Cała logika osadzona na jednej stronie, co oznacza, że logika będzie się powtarzać jeśli nowe funkcjonalności będą się pojawiać.

image

Najstraszniejsze jest jednak to, że ten wzorzec jest wpajany młodym programistom. Używałem tego wzorca na studiach bo tego można było się nauczyć na oficjalnych stronach ASP.NET w latach 2008/2009.

Na studiach nie piszę się zawansowanych aplikacji a udogodnienia wydają się korzystne. Sam na studiach zacząłem się bawić ASP.NET ledwo co znając SQL więc byłem szczęśliwy widząc kreatory, które tworzyły zapytania za mnie. Jednak już wtedy wiedziałem, że tak nie powinno się programować aplikacji. Pamiętam jak narzekałem, że mam 20 stron swoim projekcie i prawdopodobnie powinien się spokojnie zastanowić jak tą powtarzającą po raz n-ty logikę wyodrębnić, ale na studiach przynajmniej w moim wypadku nikogo nie obchodziło jak to działa i czy mam w wielu miejscach wklejony ten sam kod po 10 razy.

Smutne jest to, że czasami jest tak także w firmach. Nie ma na nic czasu więc idziemy na skróty. Gdy klient ze chce jednak dalej rozwijać aplikację wtedy zaczynają się prawdziwe problemy. Raz widziałem aplikację napisane antywzorcem Smart UI i szczerz mówiąc nie chciało mi się patrzeć na4000 linijek kodu pobocznego jednej strony “.aspx” gdzie znajdowało się *wszystko*.Jakby tego było mało pracowałem wtedy jako osoba pielęgnująca taki istniejący już kod. Wtedy nauczyłem się dlaczego wzorce projektowe mają znaczenie od najgorszej strony.

Na szczęście poważne firmy nie stosują kontrolek jak “SqlDataSource” i logika biznesowa i bazo danowa znajduje się tam gdzie powinna. Ostatnio na Polskim rynku jak i na świecie pojawiają się aplikację napisane w ASP.NET MVC. Model zdarzeniowy ASP.NET jest prosty ,ale tworzy pewne problemy jeśli chcemy naprawdę wyodrębnić aplikację na warstwy. Wiele osób życzyłoby sobie aby poważne aplikację zostały napisany w ASP.NET MVC ,a nie w klasycznym ASP.NET z jakiś zmutowanym wzorcem, który ledwo trzyma się kupy z powodu podstawowych założeń ASP.NET.

Kontrolki Telerik ratują trochę sytuacje ,ale z drugiej strony one też w swoim przykładach demo pokazują jak to wszystko niby jest fajnie gdy korzystamy z kontrolek typu “SqlDataSource”.

Kolejny powód by uczyć ASP.NET MVC.

Termin “Smart UI” pochodzi z książki “ASP.NET Desing Patterns”. W książce istnieje także bogaty opis jak unikać pułapek ASP.NET opisanych powyżej.