.NET – Anketa 1.část

Pro dnešní díl jsem si připravil příklad z reálného světa – ukážeme si, jak v ASP.NET udělat anketní systém (zatím spíše jenom anketku, v dalších dílech přibereme více funkcí).
Požadavky

Ankety určitě každý z vás zná. Představují velice příjemný způsob komunikace mezi webem a uživatelem. Uživateli stačí pouze jeden klik, aby se zapojil a správce webu může pomocí anket získat důležité informace o názorech návštěvníků.

Jaké funkce by měla obsahovat komponenta pro zobrazování anket? Zde je seznam požadavků, které považuji za důležité a rozhodl jsem se je implementovat v našem prográmku:

  • Jednoduchá možnost přidání a změny ankety (otázky, název, atd.)
  • Možnost sledovat historii minulých anket
  • Ochrana proti vícenásobnému hlasování (cookie, IP adresa)
  • Zobrazení grafu i číselné hodnoty aktuálního stavu
  • Informace o hlasujícím uživateli (čas hlasování, IP adresa)
  • Možnost zobrazit různé ankety v různých částech webu.

Jak bude výsledná aplikace fungovat, můžete vidět v ukázce. V dalším dílu si pak vytvoříme i administrativní část, která nám usnadní správu a analýzu získaných dat.

Opět si zdrojové kódy k dnešnímu dílu můžete stáhnout.

Databáze

Základem většiny aplikací je databáze, jinak tomu není ani u ankety. Nejdříve si řekněme, co bude databáze obsahovat. Vycházíme-li z požadavků uvedených výše, můžeme strukturu databáze formulovat následujícími výroky :

  • Může existovat n anket
  • Každá anketa má libovolný počet otázek
  • Každá otázka může být zvolena libovolným počtem uživatelů.

Pokud to celé přeložíme do jazyka databází, bude to vypadat asi takto:

Tabulka POLLtopic jsou vlastně témata jednotlivých anket (topic). Otázky (questions) jsou obsaženy v tabulce POLLquestion a jednotlivé volby neboli "zásahy" (hits) uživatelů jsou v poslední tabulce POLLhit. Předponu POLL (anketa) jsem zvolil schválně, protože v budoucnu možná naši aplikaci rozšíříme o více komponent.

Komponenta bude svojí architekturou 3-vrstvová. První vrstvou je právě databáze, druhou bude vrstva logických pravidel v ASP.NET.

Vrstva logických pravidel

I když název může znít strašidelně, znamená pouze pár metod pro práci s databází, které nám umožní z hlavního programu například přidat hlas pomocí jediného volání příslušné metody. Pro tuto vrstvu vytvoříme novou třídu PollDB. Pokusme se teď definovat požadavky pro takovou třídu. Když se podíváme na výslednou aplikace, zjistíme že pro zvolenou anketu, kterou chceme zobrazit, potřebujeme metody pro:

  • Zjištění názvu, popisku a celkového počtu lidí, kteří se účastnili hlasování
  • Jednotlivé otázky a příslušný počet hlasů
  • Ověření, zda už příslušný uživatel jednou nehlasoval
  • Metodu pro samotné hlasování

Ve výkladu zdrojového kódu budu navazovat na předchozí díly seriálu. Konkrétní implementace bude zahrnovat odpovídající 4 metody. Začneme první z nich:

Zjištění názvu, popisu a celkového počtu lidí, kteří se účastnili hlasování

Sub GetCurrentPoll(ByVal topicID As Integer, ByRef TopicName As String, ByRef TopicDescription As String, ByRef Total As String)
  `vytvoříme SQL dotaz pro získání sloupců Name (název ankety), Description(její popis) a Total (celkový počet hlasů).
  Dim DBCommand As OleDbCommand = PrepareSQLQuery("SELECT [Name],Description,  (SELECT   COUNT<) FROM POLLhit,POLLquestion WHERE POLLquestion.ID=POLLquestionID AND POLLtopicID=" & topicID & ") as Total  FROM POLLtopic WHERE ID=" & topicID & ";")

  `vytváříme OleDbDataReader(obdoba RecordSet z ADO), který bude obsahovat výsledek uvedeného SQL dotazu.
  `Parametr CommandBehavior.CloseConnection sděluje objektu OleDbDataReader, aby po skončení čtení automaticky uzavřel spojení do databáze
  Dim r As OleDbDataReader = DBCommand.ExecuteReader(CommandBehavior.CloseConnection)

  `Otestujme, jestli byl vůbec záznam s odpovídajícím ID nalezen, pokud ne, vytvoříme výjimku s komentářem
  If Not (r.Read()) Then Throw New Exception("No PollTopic found . Did you specify TopicID parameter ? ")

  `Nakonec přiřadíme načtené hodnoty do předaných parametrů.
  TopicName = CStr(r(0))  `mohli bychom použít TopicName = CStr(r("Name")), použití indexu je ale rychlejší
  TopicDescription = CStr(r(1)) `všechny výsledky musíme přetypovat na string pomocí CStr()
  Total = CStr(r(2)) ` poslední je celkový počet hlasujících pro tuto anketu

  r.Close() `uzavřeme OleDbDataReader, spojení se uzavře automaticky (použili jsme CommandBehavior.CloseConnection)
End Sub

Funkce GetCurrentPoll vezme parametr topicID, který obsahuje ID naší ankety záznamu v tabulce POLLtopic, a vyplní další tři parametry (TopicName, TopicDescription a Total) hodnotami, které tomuto záznamu odpovídají. Všimněte si, že pouze první parametr (topicID) je definován jako ByVal a je předáván jako hodnota. Zbývající parametry jsou předávány jako reference na daný řetězec klauzulí ByRef, což znamená, že je funkce v průběhu provádění změní (doplní data načtená z databáze). Pokud bychom neuvedli v definici žádnou z uvedených klauzulí, standardně by se parametr předal jako ByVal u primitivních typů a ByRef u objektů.

Jednotlivé otázky a příslušný počet hlasů

    Function GetResults(topicID As Integer, total As Integer) As OleDbDataReader
        Dim DBCommand As OleDbCommand

        `Vrátíme seznam otázek(POLLquestion.> a příslusné procentuelní vyjádření hlasů (... as Hits)
        `jestliže je počet hlasů 0, musíme použít jinou funkci abychom se vyhli dělení 0. Hits = (hlasy/Total)*100
        If total > 0 Then
            DBCommand = PrepareSQLQuery("SELECT POLLquestion.*, ROUND((SELECTCOUNT<) FROM POLLhit WHERE POLLhit.POLLquestionID=POLLquestion.ID)*100/" & total & ",0) as Hits FROM POLLquestion WHERE POLLtopicID=" & topicID & ";")
        Else
            DBCommand = PrepareSQLQuery("SELECT POLLquestion.*, 0 as Hits FROM POLLquestion WHERE POLLtopicID=" & topicID & ";")
        End If

        `vraťme OleDbDataReader obsahující výsledek SQL dotazu
        Return DBCommand.ExecuteReader(CommandBehavior.CloseConnection)
    End Function

Funkce GetResults nám vrátí objekt typu OleDbDataReader. Ten jsme nepřímo použili už v předchozím dílu. Je to vlastně obdoba objektu RecordSet z ASP (ADO). Umožňuje nám dopředné procházení v záznamech vrácených SQL dotazem. Co je ale mnohem důležitější: v ASP.NET tento objekt implementuje rozhraní IEnumerable, které nám umožní navázat jej jako zdroj dat pro jiné komponenty. Tak jsme například pomocí komponenty DataGrid v minulém dílu zobrazili tabulku uživatelů.

Výsledek funkce obsahuje všechny otázky a jejich příslušné procentuální vyjádření aktuálních hlasů.

Funkce pro ověření, zda již příslušný uživatel jednou nehlasoval

  Function CheckVotedIP(ByVal topicID As Integer, ByVal IPaddress As String) As Boolean
        Dim DBCommand As OleDbCommand = PrepareSQLQuery("SELECT  POLLhit.ID FROM POLLhit, POLLquestion WHERE POLLtopicID=" & topicID & " AND POLLquestionID=POLLquestion.ID AND IPaddress = `" & IPaddress & "`;")
        If CInt(DBCommand.ExecuteScalar()) <> 0 Then CheckVotedIP = True Else CheckVotedIP = False
        DBCommand.Connection.Close()
    End Function

Funkce CheckVotedIP porovná, zda uživatel s daným IP (IPaddress) už jednou v dané anketě (topicID) nehlasoval. Vrací true, jestliže uživatel svůj hlas dal, v opačném případě vrací false.

Funkce pro hlasování

    Sub Vote(ByVal questionID As Integer, ByVal IPaddress As String)
        Dim DBCommand As OleDbCommand = PrepareSQLQuery("INSERT INTO POLLhit(POLLQuestionID,VoteDateTime,IPAddress) VALUES (" & questionID & ",`" & Date.Now.ToString() & "`,`" & IPaddress & "`)")
      `ExecuteNonQuery vyvolá SQL příkaz, který nevrací žádnou hodnotu (např. UPDATE nebo INSERT)
        DBCommand.ExecuteNonQuery()
        DBCommand.Connection.Close()
    End Sub

Funkce Vote udělí hlas dané otázce (questionID). Povinný parametr IPAddress určuje IP adresu, ze které hlas přišel.

Poznámka: V této vrstvě je pouze ošetření pomocí IP adresy, protože jenom to se zapisuje do databáze. Ošetření vícenásobného hlasování pomocí cookie je až v prezentační vrstvě, protože je spjato s HTTP protokolem.

Pozorní z vás si možná všimli, že všechny funkce používají metodu PrepareSQLQuery pro vytvoření samotného dotazu. Vzhledem k tomu, že vytváření spojení a příkazu je velice často se opakující postup, vytvořil jsem tuto samostatnou metodu:

    Function PrepareSQLQuery(SQL As String) As OleDbCommand
        `vytvoří spojení s databází. ConnectionString načte z konfiguračního souboru web.config.
        Dim DBConn As New OleDbConnection(ConfigurationSettings.AppSettings.Item("ConnectionString"))

        `vytvoříme nový dotaz, parametry jsou SQL příkaz a použité spojení
        Dim DBCmd As New OleDbCommand(SQL, DBConn)

        `nakonec spojení otevřeme a vrátíme takto připravený dotaz pro použití ve volající funkci
        DBCmd.Connection.Open()
        Return DBCmd
    End Function

Ta vytvoří spojení do databáze, dotaz obsahující náš SQL příkaz a nakonec spojení otevře. Pro vytvoření spojení používá nastavení v konfiguračním souboru web.config.

Ten jsme použili pro nastavení přístupových práv k aplikaci v minulém dílu. Následující sekce předvádí, jak můžeme do tohoto souboru uložit další nastavení pro naši aplikaci:

Konfigurace

Do souboru web.config jsem přidal následující kód:

<appSettings>
    <!-- Tady nastavíme konfigurační parametry celé aplikace:
            ConnectionString        - nastavuje spojení s databází -->
   
    <add key="ConnectionString" value=" Provider=Microsoft.Jet.OLEDB.4.0; Data Source=d:\ZIVE.mdb;"/>
   
    <!-- V této sekci nastavíme parametry modulu POLL :
            POLL_VotedCheck - může nabývat hodnot :
"none"  - není žádná kotrola vícenásobného hlasování
"cookie" - kontrola pomocí cookie.
"ip"    - kontrola pomocí IP uživatele
"all"    - kontrolu pomocí cookie i IP -->
    <add key="POLL_VotedCheck" value="none"/>
</appSettings>

Sekce <appSetings/> umožňuje definici uživatelských nastavení systému. V našem případě to jsou 2 položky :

  • ConnectionString - používá se pro vytvoření spojení v PrepareSQLQuery
  • POLL_VotedCheck - používá se pro nastavení typu kontroly hlasování a může nabývat hodnot uvedených v komentářích (none, cookie, ip, all).

V samotném kódu aplikace pak máme k těmto nastavením přístup pomocí třídy ConfigurationSettings.AppSettings:

ConfigurationSettings.AppSettings.Item("ConnectionString")

Prezentační vrstva

Když už máme vytvořenou potřebnou platformu v podobě předchozích 4 funkcí ve vrstvě logických pravidel, můžeme se pustit do kódování samotné stránky:

<%@ Page Language="vb" AutoEventWireup="true" Explicit="true" Strict="true"%>
<%@ Import Namespace="System.Data.OleDb" %>
<%@ Import Namespace="System.Data" %>

<script runat="server">

`sem vložíme naše funkce z vrstvy logických pravidel.

Public TopicID As Integer = 1 ` obsahuje aktuální ID ankety pro zobrazení
Protected Voted As Boolean ` promenná obsahuje true, pokud uživatel již volil


Protected Function CheckVoted() As Boolean
`načteme nastavení z konfiguračního souboru
  Dim config As String = ConfigurationSettings.AppSettings.Item("POLL_VotedCheck")

`jestliže není nastavení provedeno správně, vyvolejme výjimku s odpovídající chybovou hláškou
  If config <> "none" And config <> "cookie" And config <> "ip" And config <> "all" Then Throw New Exception("Bad configuration in Web.config : Key POLL_VotedCheck must contain one of following values : ""none"",""cookie"",""ip"",""all""")

> `pokud nastavení vyžaduje kontrolu pomocí cookie, podívejme se, zda bylo v této anketě již hlasováno
`po každém hlasování se nastaví příslušné cookie ve formátu P1xxx , kde xxx je ID daného záznamu v tabulce POLLtopic
  If config = "cookie" Or config = "all" Then
    If Not (Request.Cookies("P1" & TopicID) Is Nothing) Then CheckVoted = True
  End If

`jestliže je vyžadována kontrola podle IP adresy, otestujeme uživatele pomocí funkce CheckVotedIP
`IP adresu aktuálního klienta máme ve Request.UserHostAddress
  If (config = "ip" Or config = "all") And CheckVoted = False Then
CheckVoted = PollDB.CheckVotedIP(TopicID, Request.UserHostAddress)
  End If
End Function

Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs)
`při každém načtení zjistíme, zda uživatel už hlasoval
Voted = CheckVoted()

`při prvním načtení stránky načteme všechna data do prvků na stránce a zobrazíme je
`při dalších voláních již VIEWSTATE (skrytý input v HTML) obsahuje zakódaovaná data v prvcích,
`takže se již o ně starat nemusíme.
`výhodou je pouze jedno volání metody GetResult, čímž se méně zatěžuje databáze.
If Not IsPostBack Then
PollDB.GetCurrentPoll(TopicID, TopicName.Text, TopicDescription.Text, TotalVotes.Text)
  Repeater1.DataSource = PollDB.GetResults(TopicID, CInt(TotalVotes.Text))
    Me.DataBind()
  End If
End Sub

Protected Sub Repeater1_ItemCommand(ByVal source As System.Object, ByVal e As System.Web.UI.WebControls.RepeaterCommandEventArgs)

> `nejdříve ověříme, zda uživatel ješte nehlasoval, proměnná Voted byla inicializována ve funkci Page_Load,
`která se vždy vykoná jako první. Voted je tedy true, pokud uživatel už hlasoval, jinak false.
If Not (Voted) Then

  `začínáme hlasování, nejdříve vytvoříme cookie, abychom uložili informaci o hlasování na klientovi
    `cookie je ve formátu P1xxx , kde xxx je ID daného záznamu v tabulce POLLtopic - tím rozlišíme mezi
  `hlasováním v různých anketách, pokud jich máme více.

    Dim c As New HttpCookie("P1" & TopicID)

    `nastavíme dobu vypršení na jeden měsíc
    c.Expires = Now.AddMonths(1)

    `a přidáme do objektu Response. Ten ji přetranformuje na příslušný HTTP požadavek již automaticky
    Response.Cookies.Add(c)

    `nakonec zavoláme funkci Vote a předáme mu ID zvolené otázky získané z komponenty Repeater, a klientovu IP adresu
    PollDB.Vote(CInt(e.CommandArgument), Request.UserHostAddress)

    `zvýšíme počítadlo hlasujících lidí TotalVotes (komponenta typu Label)
    TotalVotes.Text = CStr(CInt(TotalVotes.Text) + 1)

    `jestliže je zapnuta kontrola vícenásobného hlasování, je potřeba nastavit Voted=true.
    `pokud zapnuta není, chceme naopak nechat možnost ještě volit.
    If ConfigurationSettings.AppSettings.Item("POLL_VotedCheck") <> "none" Then Voted = True

  `aktualizujme data pro komponentu Repeater1
    Repeater1.DataSource = PollDB.GetResults(TopicID, CInt(TotalVotes.Text))
    Repeater1.DataBind()
  End If
End Sub
`---------------------------------------  Konec VB.NET části ---------------------------------------
</script>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
<HEAD>
<title>Anketa</title>
<link REL="stylesheet" TYPE="text/css" href="css/main.css">
</HEAD>
<body>
<form id="Form1" method="post" runat="server">
<TABLE cellpadding="0" cellspacing="0">
<tr>
<td class="elementTitle">
<asp:Label id="TopicName" runat="server" /></td>
</tr>
<tr>
<td class="elementSubTitle"><asp:Label id="TopicDescription" runat="server" /></td>
</tr>
<asp:repeater id="Repeater1" runat="server" OnItemCommand="Repeater1_ItemCommand">
<ItemTemplate>
<TR>
<TD colspan="2">
<asp:LinkButton CssClass="elementLink" Enabled=`<%#not(Voted)%>` id="Linkbutton1" Runat="server" CommandName="Vote" CommandArgument=`<%# DataBinder.Eval(Container.DataItem, "ID") %>` Text=`<%#DataBinder.Eval(Container.DataItem, "Question")  %>`/>
</TD>
</TR>
<TR>
<TD width="210px">
<asp:LinkButton Enabled=`<%#not(Voted)%>` id=Vote Runat="server" CommandName="Vote" CommandArgument=`<%# DataBinder.Eval(Container.DataItem, "ID") %>`>
<asp:Image id=ImageButton1 runat="server" ImageUrl="img/bar.gif" Width=`<%# Web.UI.WebControls.Unit.Parse(CStr(2*Cint(DataBinder.Eval(Container.DataItem, "Hits")))) %>` Height="8px"/>
<asp:Image id="ImageButton2" runat="server" ImageUrl="img/bar_end.gif" Width="5px" Height="8px" />
</asp:LinkButton></TD>
<td align="left" width="20px">
<asp:LinkButton CssClass="elementLink" Enabled=`<%#not(Voted)%>` id="Linkbutton2" Runat="server" CommandName="Vote" CommandArgument=`<%# DataBinder.Eval(Container.DataItem, "ID") %>` Text=`<%#CStr(DataBinder.Eval(Container.DataItem, "Hits"))&"%"%>`/></td>
</TR>
</ItemTemplate>
</asp:repeater>
<tr>
<td colspan="2" class="elementText">Celkove hlasovalo <asp:Label ID="TotalVotes" Runat="server" /> lidí</td>
</tr>
</TABLE>
</form>
</body>
</HTML>

I dnes si rozebereme programový kód a HTML část zvlášť. Kód programu obsahuje 3 funkce :

  • CheckVoted - funkce zjišťuje podle aktuálního nastavení (viz. Sekce konfigurace), zda již uživatel hlasoval. Vrátí true pokud ano.
  • Page_Load - vykonává se při každém načtení stránky a má za úkol provést inicializaci dat.
  • Repeater1_ItemCommand - funkce provádí samotné hlasování pro kliknutí uživatele na danou otázku.

Vysvětlení jak, jednotlivé funkce pracují naleznete v komentářích kódu. Podrobněji se později podíváme na funkci Repeater1_ItemCommand obsahující pár zvláštností, které stojí za objasnění. Před tím si ale musíme udělat krátký úvod do komponenty Repeater.

Repeater

Repeater v podstatě nahrazuje smyčku v ASP, ve které bychom normálně zobrazili HTML pro jednotlivé řádky z databáze. Přistupuje ale k problému z pohledu ASP.NET.

Nejdříve si vytvoříme šablonu (template) pro jeden záznam. Poté navážeme na Repeater datový zdroj - například výsledek naší funkce GetResults. Repeater pak doplní data z datového zdroje do šablony pro každý záznam. Jak to vypadá v reálu ? Datový zdroj navážeme pomocí vlastnosti DataSource:

Repeater1.DataSource = GetResults(TopicID, CInt(TotalVotes.Text))
Repeater1.DataBind()

Metoda DataBind() uskuteční samotné provázání dat. Podívejme se teď na ukázkovou šablonu:

<asp:repeater id="Repeater1" runat="server">
<ItemTemplate>
<asp:Label id="Otazka" Runat="server" Text=`<%#DataBinder.Eval(Container.DataItem, "Question") %>`/><br>
</ItemTemplate>
</asp:repeater>

Tato mini-šablona nám vypíše pro všechny záznamy pod sebe komponentu Label, která obsahuje text otázky. Proč je tam ten krkolomný výraz

DataBinder.Eval(Container.DataItem, "Question")

Ten nám umožní navázat data ze zadaného sloupce Question. Pokud se podíváme na SQL dotaz, který nám vytvořil datový zdroj ve funkci GetResults vypadá asi takto:

SELECT * , ... FROM POLLquestion WHERE ...

Mezi vybranými sloupci je tedy i sloupec Question z tabulky POLLquestion. Právě na něj odkazujeme v příkazu DataBinder.Eval. Pokud se vám použití příkazu DataBinder.Eval zdá zmatečné, nezoufejte. Časem si vysvětlíme proč vypadá tak, jak vypadá, zatím jej berte jako fakt :).

Probublávání událostí

Tento velice technicky znějící výraz nám umožňuje reagovat na událost uvnitř Repeateru. Vezněme si příklad šablony:

<asp:repeater id="Repeater1" runat="server">
<ItemTemplate>
<asp:Button id="OtazkaButton" Runat="server" Text=`<%#DataBinder.Eval(Container.DataItem, "Question") %>`/><br>
</ItemTemplate>
</asp:repeater>

Vytvoří nám několik tlačítek s možností hlasování pro jednotlivou otázku. Jak ale zjistíme, které z tlačítek bylo stlačeno, pokud ID u všech je stejné - OtazkaButton?

Řešení problému je vytvořit jednu funkci pro obsluhu události ItemCommand komponenty Repeater a ve funkci ošetřit, které z tlačítek bylo vybráno :

<script runat="server">
Protected Sub Repeater1_ItemCommand(ByVal source As System.Object, ByVal e As System.Web.UI.WebControls.RepeaterCommandEventArgs)
TotalVotes.Text="Otazku volilo " & Cstr(e.CommandArgument) & " % lidí"
End Sub
</script>

<asp:repeater id="Repeater1" runat="server" OnItemCommand="Repeater1_ItemCommand">
<ItemTemplate>
<asp:LinkButton  id="Linkbutton2" Runat="server" CommandArgument=`<%# DataBinder.Eval(Container.DataItem, "Hits") %>` Text=`<%#(DataBinder.Eval(Container.DataItem, "Question"))%>`/><br>
</ItemTemplate>
</asp:repeater>
<asp:Label id="TotalVotes" runat="server"/>

V uvedeném příkladu jsem navázal vlastnost CommandArgument objektu LinkButton na sloupec Hits. V něm SQL dotaz vrátil, kolik procent lidí volilo danou otázku. To, co předáme v CommandArgument, můžeme pak vyzvednout v naší funkci pomocí příkazu

e.CommandArgument

Pokud bychom poslali například ID dané otázky, mohli bychom hlasovat pomocí funkce Vote tak, jak je to ve výsledné anketě:

Vote(CInt(e.CommandArgument), Request.UserHostAddress)

Diskuse na závěr

Systém, který jsme dnes vytvořili má několik much. Snažil jsem se o co nejjednodušší výklad zdrojového kódu a postupu, takže jsem musel některé věci vynechat.

Přístup k zabezpečení vícenásobného volení není úplně dostačující. Můžou nastat následující krajní situace :

  • Uživatel má zakázané cookies - umožní mu volit vícekrát v případě zapnutí pouze kontroly pomocí cookies.
  • Uživatel přistupuje na stránku ze sdílené IP adresy (např. proxy server pro celou organizaci). Pokud je zapnutá kontrola IP adresy, nemůžou dva uživatelé ze stejné IP adresy organizace hlasovat.
Několik řešení by bylo:

  • Nepustit uživatele k hlasování, pokud jeho prohlížeč nepodporuje cookies.
  • Před hlasováním uživatele přihlásit do systému.
Každé z těchto řešení má své přínosy, ale ani jedno není dokonalé. S tím se už musíme holt smířit, že ochrana soukromí uživatele je v tomto případě omezující.

Co zbývá pro příště ?

  • Vytvoříme si z dnešního kódu komponentu, kterou budeme moci nezávisle vkládat do našich stránek. Umožní nám to ku příkladu mít více anket na jedné stránce bez změn v kódu komponenty.
  • Je potřeba vylepšit algoritmus pro výpočet procent - někdy součet nedá 100%.
  • Graf by také vypadal hezčeji, pokud by byl vždy roztažen na plnou délku u největšího počtu hlasů. Ostatní by se roztáhly relativně k této největší hodnotě.
  • No a hlavně začneme pracovat na administrativní části, která je stejně důležitá, jako zobrazení samotné ankety.
Diskuze (23) Další článek: Microsoft uchovává seznam přehraných titulů

Témata článku: , , , , , , , , , , , , , , , , , , , , , , , , ,