Aplikace pro zamlouvání sedadel (část 3)

3. díl - Aplikace pro zamlouvání sedadel (část 3)

Tomáš Herceg       13.02.2011       C#, VB.NET, ASP.NET WebForms, ASP.NET/IIS, .NET       12451 zobrazení

V této části si ukážeme, jak vygenerovat a naplnit tabulku, a jak napsat komponentu, která má vlastní serverové události. Nakonec si ukážeme, jak aktualizovat jen část stránky pomocí komponenty UpdatePanel.

V minulých dvou dílech jsme připravili základní infrastrukturu naší aplikace a napsali třídu, která zprostředkovává práci s daty – získává informace o koncertních místnostech, obsazenosti sedadel a provádí jejich rezervace.

Vytvoření tabulky

Nyní se vrátíme do stránky Default.aspx a budeme chtít naplnit naši tabulku. Pro její reprezentaci používáme komponentu Table, která má kolekci Rows, jež očekává objekty TableRow – řádky tabulky. A řádek má vlastnost Cells, která je určena pro objekty typu TableCell.

V metodě Page_Load tedy zavoláme GetHall a vygenerujeme prázdné buňky tabulky. Plnit je budeme až v metodě Page_LoadComplete, protože pokud se zavolá událost Click, bude to mezi těmito dvěma metodami. Aby se událost vyvolala, musí ve stránce existovat komponenta, která ji vyvolala (buňka tabulky, na kterou se kliknulo). Takže buňky musíme vytvořit předtím, kdežto aktuální stav můžeme načítat až potom – jinak by nebyl úplně aktuální.

Tento vzor je dobré si zapamatovat, protože se v ASP.NET používá velmi často – například pokud ve stránce máte Repeater a SqlDataSource, tak při prvním načtení stránky databinding probíhá až ve fázi PreRender (což je těsně před generování HTML výstupu). Ale při postbacku se musí obsah Repeateru postavit a obnovit už ve fázi Load, protože jinak by se nemohly vyvolat události z komponent uvnitř položek Repeateru.

Metoda Page_Load ve stránce Default.aspx bude tedy vypadat takto:

     private const int hallId = 1;
private SeatManager seatManager = new SeatManager();

/// <summary>
/// Vygenerovat buňky tabulky
/// </summary>
protected void Page_Load(object sender, EventArgs e)
{
// vytáhnout detail koncertního sálu
Hall hall = seatManager.GetHall(hallId);

// vygenerovat buňky tabulky
for (var rowIndex = 0; rowIndex < hall.SeatRows; rowIndex++)
{
// vytvořit řádek tabulky
var row = new TableRow();
SeatsTable.Rows.Add(row);

for (var columnIndex = 0; columnIndex < hall.SeatColumns; columnIndex++)
{
// vytvořit buňku
var cell = new ClickableTableCell();
cell.ID = string.Format("Seat{0}_{1}", rowIndex, columnIndex);
cell.Text = string.Format("{0}-{1}", rowIndex + 1, columnIndex + 1);
cell.Click += new EventHandler(SeatCell_Click);
row.Cells.Add(cell);
}
}
}

/// <summary>
/// Ošetří kliknutí na buňku tabulky
/// </summary>
protected void SeatCell_Click(object sender, EventArgs e)
{
}

V metodě Page_Load jsme si nejdřív vytáhli objekt reprezentující koncertní síň (napevno zde máme ID 1, máme jen jednu síň, ale v praxi by se toto ID získalo například z parametru v URL).

Dále zde máme dva vnořené cykly – vnější generuje řádky a vnitřní do každého řádku vygeneruje jednotlivé buňky. Buňka není typu TableCell, ale ClickableTableCell, což je naše třída, kterou napíšeme později. Do buňky napíšeme číslo řady a číslo sloupce, a při kliknutí na buňku chceme zavolat metodu SeatCell_Click.

Je naprosto nutné každé vygenerované buňce nastavit ID, a to deterministicky, tedy aby bylo při každém postbacku stejné. Jinak nám serverové události nebudou fungovat. Pokud se ve stránce nenajde komponenta s ID, které měla komponenta, jíž byl postback vyvolán, událost se zahodí a nic se nestane.

Naplnění tabulky daty

Nyní napíšeme metodu, která bude plnit tabulku daty. Ta jen použije datovou třídu a nastavi buňce CSS třídu a ToolTip text.

     /// <summary>
/// Naplní tabulku daty
/// </summary>
protected void Page_LoadComplete(object sender, EventArgs e)
{
// nastavit data buněk tabulky
foreach (var seat in seatManager.GetSeats(hallId))
{
// najít buňku v tabulce
var cell = SeatsTable.Rows[seat.Y].Cells[seat.X] as ClickableTableCell;

if (seat.UserId == null)
{
// místo je volné
cell.CssClass = "free";
}
else if (string.Equals(seat.User.UserName, Page.User.Identity.Name, StringComparison.OrdinalIgnoreCase))
{
// místo je naše
cell.CssClass = "yours";
cell.ToolTip = "Toto místo je vaše.";
}
else
{
// místo je zabrané někým jiným
cell.CssClass = "reserved";
cell.ToolTip = seat.User.UserName;
}

// nastavit buňce ID sedadla (budeme jej potřebovat v události Click)
cell.SeatId = seat.SeatId;
}
}

Z naší datové třídy získáme kolekci sedadel v naší místnosti. Pro každé sedadlo najdeme buňku v tabulce a podle jeho stavu nastavíme příslušnou CSS třídu. Pokud je místo někoho jiného, nastavíme též ToolTip text (objeví se při najetí myši) na jméno daného uživatele.

Zároveň si do vlastnosti SeatId, kterou za chvíli přidáme do třídy ClickableTableCell, poznamenáme ID sedadla, abychom buňku, na níž jsme kliknuli, mohli posléze identifikovat v metodě SeatCell_Click.

Rezervace sedadla

Skoro poslední věc, kterou zde musíme udělat, je doplnit funkcionalitu pro rezervaci sedadla. Metoda SeatCell_Click bude vypadat takto:

     /// <summary>
/// Ošetří kliknutí na buňku tabulky
/// </summary>
protected void SeatCell_Click(object sender, EventArgs e)
{
// přetypovat objekt na buňku v tabulce, abychom zjistili SeatId
var cell = sender as ClickableTableCell;

// zkusit rezervovat místo
if (seatManager.TryReserveSeat(cell.SeatId, Page.User.Identity.Name))
{
ErrorLabel.Text = "Sedadlo bylo rezervováno.";
}
else
{
ErrorLabel.Text = "Sedadlo se nepodařilo zarezervovat, vlastní jej někdo jiný.";
}
}

Podle toho, jestli se rezervace povede či ne, zaktualizujeme text v komponentě ErrorLabel. Tato komponenta má vypnutý ViewState, takže pokud uděláme postback jinak než touto událostí, text uvnitř ní se zapomene. Toto chování je jako dělané pro tyto “statusové” komponenty.

Vlastní buňka s podporou události Click

Nyní nám zbývá poslední část skládačky, a to je buňka tabulky se serverovou událostí Click. Obyčejná třída TableCell je jen tenký wrapper nad HTML elementem <td></td>, takže neobsahuje serverové události. Jejich podporu si musíme dopsat sami. A jak na to?

Vytvořte třídu s názvem ClickableTableCell, která dědí z TableCell. Aby mohla komponenta mít vlastní události, je třeba, aby implementovala rozhraní IPostBackEventHandler. To předepisuje jedinou metodu RaisePostBackEvent, ve které máte za úkol vyvolat příslušnou událost. Každá událost může mít ještě argument. To je například pro situace, kdy byste chtěli mít serverových událostí více, například Click a KeyDown. Podle argumentu rozlišíte, o jakou událost jde, případně si tam můžete poslat jakoukoliv hodnotu, která se vám hodí.

Aby kliknutí na buňku tabulky ve stránce událost vyvolalo, je potřeba elementu td přidat atribut onclick a v něm javascriptem zavolat onu známou metodu __doPostBack. Tento javascript ale nebudeme psát ručně - necháme si ho vygenerovat metodou Page.ClientScript.GetPostBackEventReference.

Protože ASP.NET obsahuje navíc mechanismus zvaný Event Validation, který zabraňuje útočníkům poslat škaredý HTTP request, který vyvolá událost, která být z dané stránky vyvolaná nemohla, je nutné ještě naši vlastní událost zaregistrovat. To dělá přímo metoda GetPostBackEventReference, ale musíme ji zavolat až ve chvíli, kdy se stránka renderuje.

Ideální místo je metoda AddAttributesToRender. Ta se zavolá v okamžiku, kdy se renderuje element <td> a má za úkol nastavit všechny potřebné atributy. Buňce jsme například nastavili vlastnost CssClass, takže tato metoda by měla nastavit atribut class atd. To ale zařídí třída TableCell, takže my k výchozí implementaci jen přidáme náš atribut onclick.

Do třídy ClickableTableCell tedy přidejte tento kód.

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.UI.WebControls;
using System.Web.UI;

/// <summary>
/// Buňka tabulky, která obsahuje serverovou událost Click
/// </summary>
public class ClickableTableCell : TableCell, IPostBackEventHandler
{

#region Událost click

/// <summary>
/// Je vyvolána v případě, že uživatel na komponentu klikne
/// </summary>
public event EventHandler Click;

/// <summary>
/// Vyvolá událost Click
/// </summary>
protected virtual void OnClick(EventArgs e)
{
if (Click != null)
Click(this, e);
}

/// <summary>
/// Vyvolá událost Click, pokud nastala
/// </summary>
public void RaisePostBackEvent(string eventArgument)
{
if (eventArgument == "Click")
{
OnClick(EventArgs.Empty);
}
}

/// <summary>
/// Těsně před renderováním nastavit javascript, který při kliknutí na buňku událost vyvolá
/// </summary>
protected override void AddAttributesToRender(HtmlTextWriter writer)
{
writer.AddAttribute("onclick", Page.ClientScript.GetPostBackEventReference(this, "Click", true));

base.AddAttributesToRender(writer);
}

#endregion
}

Jak je vidět, začínáme dvojicí událost Click a metoda OnClick, která tuto událost vyvolá (jako sender jí předá this). Dále máme metodu RaisePostBackEvent, která podle event argumentu vyvolá událost Click. Pokud by někdo podvrhnul jiný argument, nic se nestane.

V metodě AddAttributesToRender přidáme atribut onclick, metoda GetPostBackEventReference bere jako parametr komponentu, která událost vyvolává, dále event argument příslušné události, a poslední parametr říká, jestli tuto událost chceme zaregistrovat pro event validation.

Nezapomeneme zavolat base.AddAttributesToRender, aby se přidaly i ostatní atributy, např. výše zmiňovaný class.

Control State

Poslední věc, kterou má naše klikací buňka umět, je pamatovat si ID sedačky. Vzhledem k tomu, že bez ID sedačky fungovat nemůžeme, musíme jej uložit minimálně do ViewState, ale vzhledem k tomu, že ten budeme chtít vypnout, dáme ji do Control State. To je část ViewState, která se vypnout nedá, a která je nezbytná pro fungování stránky či komponenty.

Dělá se to pomocí tří kroků – nejdřív je nutné v události OnInit oznámit stránce, že Control State potřebujeme. Později (například v OnLoad) to nemá smysl, jelikož Control State a ViewState se načítá ještě před událostí Load.

Pak je třeba přepsat metody LoadControlState a SaveControlState. Vzhledem k tomu, že nevíme, jestli předek naší třídy TableCell nějaký stav má, musíme vždy volat base.LoadControlState a base.SaveControlState a musíme vždy ukládat a načítat stav náš i stav našeho předka.

Ideálním prostředkem pro tuto věc je třída Pair, která má vlastnosti First a Second. Při ukládání stavu tedy vytvoříme dvojici stavu předka a mého vlastního, a při načítání, ji zase rozebereme.

ID sedačky si budeme pamatovat v obyčejné vlastnosti SeatId typu int. Nemusíme v getteru a setteru ukládat nic do ViewState, na to máme ControlState.

Do třídy ClickableTableCell tedy přidejte tento kód:

     #region ID sedadla

/// <summary>
/// ID sedadla, které tato buňka reprezentuje.
/// </summary>
/// <remarks>Tato vlastnost se ukládá do control state, je nutné ji uchovat pro postbacky.</remarks>
public int SeatId { get; set; }

/// <summary>
/// Říct, že potřebujeme control state
/// </summary>
protected override void OnInit(EventArgs e)
{
Page.RegisterRequiresControlState(this);
base.OnInit(e);
}

/// <summary>
/// Načte control state
/// </summary>
protected override void LoadControlState(object savedState)
{
// control state je dvojice - první část je stav předka (třídy TableCell),
// druhá část je můj vlastní stav (stav třídy ClickableTableCell)
Pair p = (Pair)savedState;
base.LoadControlState(p.First);
SeatId = (int)p.Second;
}

/// <summary>
/// Uloží control state
/// </summary>
protected override object SaveControlState()
{
return new Pair(base.SaveControlState(), SeatId);
}

#endregion

Poslední funkce – AJAX

Nyní by nám měla celá aplikace fungovat – spusťte ji ve více různých prohlížečích, vytvořte několik uživatelů a zarezervujte pár míst. Vyzkoušejte, že máme vyřešenou konkurenci – v jednom zarezervujte místo a pak to samé zkuste zarezervovat i ve druhém, který nezobrazuje aktuální stav. Měla by nastat chyba a objeví se hláška, že se rezervace nepovedla.

Poslední úprava, kterou můžeme udělat, je zařídit, aby se kliknutím neodesílala celá stránka, ale jen tabulka. V naší konkrétní aplikaci to žádné úspora nebude, protože na stránce nic jiného není, ale kdyby zamlouvání sedadel byla jen menší část obsahu, smysl to už má.

Stačí část stránky, kterou chceme aktualizovat, obalit do komponenty UpdatePanel. A aby uživatel viděl, že se něco děje, můžeme použít i UpdateProgress, který zobrazí svůj obsah v době, kdy právě probíhá postback.

Při kliknutí na komponentu uvnitř UpdatePanelu (anebo na komponentu, která je zaregistrovaná v sekci Triggers tohoto UpdatePanelu) se nebude odesílat celá stránka, ale pouze se pomocí javascriptu vytvoří HTTP požadavek. Ten se odešle na server (včetně cookies, formulářových dat a ViewState, na to pozor), ASP.NET jej zpracuje jako kdyby to byla normální stránka, akorát jako výstup vrátí jen obsah UpdatePanelu (nebo více UpdatePanelů, které na stránce máme). V našem případě se nám vrátí jen obsah tabulky, který se pomocí javascriptu nahradí za to, co je na daném místě ve stránce v tuto chvíli.

Aby tyto komponenty fungovaly, je třeba do stránky umístit ještě komponentu ScriptManager, a to hned na začátek dovnitř elementu form. Pokud používáte Master Pages, stačí jej umístit tam, ale musí být opravdu hned na začátku. Tato komponenta se stará o přidání potřebných skriptů do stránky.

Zároveň můžeme na naší tabulce zakázat ViewState, protože jej na nic nepotřebujeme – jestli je buňka obsazená nebo ne si zjistíme z databáze, a její text a tooltip stejně nastavujeme v každém požadavku, takže to není třeba do ViewState ukládat. Na funkci serverových událostí to nemá vliv a jediné, co potřebujeme zachovat, totiž SeatId, se ukládá do ControlState, který se vypnout nedá. Navíc tato ID ukládáme bezpečně (zašifrovaně, zkomprimovaně a digitálně podepsaně) v poli __VIEWSTATE a ne například v javascriptu v elementu onclick nebo v obyčejném skrytém poli. Nikdo nám tedy nepodvrhne ID sedadla, které na stránce nevidí, a teoreticky to v aplikaci nemusíme ošetřovat.

V praxi bych to ale stejně udělal, toto je však jen příklad.

Stránka Default.aspx nyní bude vypadat takto:

 <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Opera - zamlouvání sedadel</title>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server" />
<div>

<h1>Zamlouvání sedadel</h1>

<asp:UpdatePanel ID="SeatsUpdatePanel" runat="server" ChildrenAsTriggers="true">
<ContentTemplate>

<p><asp:Label ID="ErrorLabel" runat="server" ForeColor="Red" ViewStateMode="Disabled" /></p>

<asp:Table ID="SeatsTable" runat="server" CssClass="seats" ViewStateMode="Disabled">
</asp:Table>

</ContentTemplate>
</asp:UpdatePanel>

<asp:UpdateProgress ID="UpdateProgress" runat="server">
<ProgressTemplate><p>Čekejte, prosím...</p></ProgressTemplate>
</asp:UpdateProgress>

<p class="legend">
<span class="reserved"></span> - obsazené sedadlo<br />
<span class="free"></span> - volné sedadlo<br />
<span class="yours"></span> - vaše sedadlo
</p>

</div>
</form>
</body>
</html>

A abyste viděli UpdateProgress v akci i na lokálním webserveru, přidejte do metody SeatCell_Click tento řádek, kterým nasimulujeme zpoždění (pak jej ale zase dejte pryč). Ideální je dovnitř této komponenty dát takový ten obrázek světlušky jak lítá pořád dokola.

 System.Threading.Thread.Sleep(2000);

Výsledek vypadá takto:

image[11]_thumb

Vždy doporučuji UpdatePanely přidávat až na konec, protože ladění chyb je s nimi daleko obtížnější – zobrazí se jen někdy a nejsou tak podrobná.

 

Na této jednoduché aplikaci jsme si ukázali několik věcí – seznámili jsme se s technologií LINQ to SQL, která se dá použít pro snazší práci s databází (i když na první pohled vypadá komplikovaně). Dále jsme si ukázali, jak vlastní komponentě přidat serverovou událost a jak tuto událost vyvolat z javascriptu na klientovi. A nakonec jsme si ukázali, jak využít AJAX a aktualizovat ve stránce jen to, co má smysl.

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

 

Všechny díly tohoto seriálu

4. Jak na dlouhotrvající úlohy 26.01.2012
3. Aplikace pro zamlouvání sedadel (část 3) 13.02.2011
2. Aplikace pro zamlouvání sedadel (část 2) 08.02.2011
1. Aplikace pro zamlouvání sedadel (část 1) 04.02.2011

 

 

 

Nový příspěvek

 

Diskuse: Aplikace pro zamlouvání sedadel (část 3)

Musím poděkovat za výborný článek.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Diskuse: Aplikace pro zamlouvání sedadel (část 3)

Zaujimavym pokracovanim by bol clanok s trochu pokrocilejsou tematikou - ak si niekto obsadi (zamluvi) sedadlo. server posle notifikaciu vsetkym klientom. V sucasnej dobe to nieje take jednoduche, ale da sa to a prave preto, by takyto navod mohol prist mnohym vhod.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Díky za nápad, o tom by se jistě něco napsat dalo, jakmile budu mít čas, podívám se na to.

nahlásit spamnahlásit spam 1 / 1 odpovědětodpovědět

Diskuse: Aplikace pro zamlouvání sedadel (část 3)

Děkuji autorovi za článek. Rád bych se zeptal zda-li budou vaše další seriály o LINQ to SQL navazovat na tento příklad. Jedná se mi především o to, jak třeba např. aplikaci vylepšit o to, aby uživatel mohl zarezervovat vždy jenom jedno místo, nebo aby se místo čísla ukázalo konkrétní jméno uživatele, který sedadlo zarezervoval, popřípadě, aby uživatel mohl rezervaci zrušit. Děkuji za odpověď.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Seriál o LINQ to SQL mám v plánu, ale nevím, kdy se k němu dostanu.

Aby každý mohl mít jedno sedadlo, to by se dalo řešit jinou strukturou databáze (SeatId v tabulce Users), anebo pomocí UNIQUE klíče v databázi na sloupečku UserId v tabulce Seats.

Odrezervování místa nebo zobrazení jména z tabulky profilů uživatele, to už si každý dodělá sám, bylo by to jen nošení dříví do lesa.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Děkuji za odpověď. Už se s tím zkouším prát, ale bohužel se mi nějak nedaří nastavit ten UNIQUE KEY na sloupečku UserId v tabulce Seats. Když ho nastavím tak stále mohu rezervovat další místa. Dělám někde chybu, ale nevím kde. Ale i tak děkuji, budu to zkoušet dál :-).

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Diskuse: Aplikace pro zamlouvání sedadel (část 3)

Dobrá práce jen tak dál...

nahlásit spamnahlásit spam 0 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback