wtorek, 29 marca 2011

Obsługa strony z pytaniami - Radio Buttons w ASP MVC 2

Dzisiaj trochę o stworzeniu w MVC 2 kontrolera obsługującego przyciski typu radio.
Przyciski radio button to nic innego jak przyciski wyboru. Na wielu stronach można spotkać się z listami wyboru typu: kobieta/mężczyzna. Przykład? Ot chociażby zakładanie konta pocztowego w portalu onet:


W swoim ostatnim projekcie w MVC potrzebowałem całego formularza takich kontrolek. Z pozoru zadanie było bardzo proste i sprowadzało się do:
"Potrzebna jest formatka, na której wyświetlą się przyciski TAK/NIE dla listy pytań". Tak więc należało stworzyć mechanizm, który pobiera pytania z bazy danych, tworzy listę do wyboru i zapisuje do bazy danych rezultat.
Proste i nieskomplikowane zadanie na pierwszy rzut oka okazało się nie być takie oczywiste w późniejszym etapie i kolejnych życzeniach klienta.

Oczywiście w programowaniu, tak jak i matematyce, problem można rozwiązać na wiele sposobów. Ja przedstawię tutaj dwa sposoby, które sprawdzają się w moich projektach. Pierwszy przedstawiony sposób jest bardzo podobny do tego stosowanego w PHP (bezpośrednie odwołanie się do konkretnych pól formularza), drugi używa mechanizmu bindowania wbudowanego w MVC.

Sposób I

A więc rozpoczynamy, na potrzeby tego artykułu stworzymy plik XML z pytaniami o bardzo uproszczonym schemacie. Tworzymy więc na początek prosty plik XML Questions.XML o zawartości:



<?xml version="1.0" encoding="utf-8" ?>
<Questions>
  <Question>
    <Id>1</Id>
    <Content>Czy podoba Ci się ten blog?</Content>
  </Question>
  <Question>
    <Id>2</Id>
    <Content>Lubisz swoją uczelnie?</Content>
  </Question>
  <Question>
    <Id>3</Id>
    <Content>Mieszkasz w fajnej dzielnicy?</Content>
  </Question>
  <Question>
    <Id>4</Id>
    <Content>Chodzisz do kina?</Content>
  </Question>
  <Question>
    <Id>5</Id>
    <Content>Uprawiasz sport?</Content>
  </Question>
</Questions>


Każde pytanie ma swoją treść (content) oraz niepowtarzalny identyfikator (id). Tworzymy więc bardzo prosty model naszego pytania:



using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace RadioButtonsExample.Models
{
    public class Question
    {
        public int Id { get; set; }
        public string Content { get; set; }
    }
}

Następnie tworzymy kontroler - Home. W metodzie Index wprowadzimy następujący kod:


Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Xml.Linq;
using RadioButtonsExample.Models;

namespace RadioButtonsExample.Controllers
{
    public class HomeController : Controller
    {
        //
        // GET: /Home/

        public ActionResult Index()
        {
            XDocument xmlQuestionDocument = XDocument.Load("Questions.xml");
            IList<Question> lQuestions = new List<Question>();
            var questions = from a in xmlQuestionDocument.Elements()
                            select a;
            foreach (var item in questions.Elements("Questions.xml"))
            {
                lQuestions.Add(new Question { Id = int.Parse(item.Element("Id").Value), Content = item.Element("Content").Value });
            }

            return View(lQuestions);
        }

    }
}

Tak więc do widoku zostanie przekazana kolekcja pytań. Widok tworzymy oczywiście silnie typowany:


Code:
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<IEnumerable<RadioButtonsExample.Models.Question>>" %>

<!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>Index</title>
</head>
<body>
    <div>
    <% using (Html.BeginForm())
       { %>
       <% foreach (var item in Model)
          { %>
          <%: item.Id.ToString() + ". " + item.Content %>
          <br />
          <%: Html.RadioButton("P" + item.Id.ToString(), "T", true) %> TAK<br />
          <%: Html.RadioButton("P" + item.Id.ToString(), "N", false) %> NIE<br />
          <br />
       <% } %>
       <br />
       <br />
       <input type="submit" value="Wyślij" />
    <% } %>
    </div>
</body>
</html>

ASP.MVC posiada bardzo silny model bindingu. W łatwy sposób możemy bindować całe obiekty do formularzy i je zwracać w postaci obiektu. W tym przypadku, łatwiej jest skorzystać z kolekcji - FormCollection. Zawiera ona klucze i wartości pól na formularzu.

Wyświetlanie: zakładamy na początku najprostszą wersję:
- Wszystkie pytania mają domyślnie zaznaczoną odpowiedź.
W tym przypadku odbieramy rezultat jako kolekcję kluczy formularza. Ważne jest tu słowo kluczy - które jest utożsamione z nazwą konkretnego pola na formularzu. Pola na formularzu są budowane w sposób następujący: P{id} czyli dla pytania pierwszego mamy P1, drugiego P2 itd.
W przypadku gdy nie wybierzemy zaznaczenia domyślnego dla jednej z odpowiedzi, a użytkownik nie zaznaczy żadnej z odpowiedzi w kolekcji kluczy nie znajdziemy interesującej nas pozycji. W takiej sytuacji pozostaje sprawdzenie czy kolekcja zawiera żądaną przez nas wartość. Ja w tym celu postanowiłem stworzyć dodatkową klasę, która tworzy słownik z kontrolkami które są na formularzu i pozwala w bardzo szybki sposób sprawdzić, czy żądany klucz jest w kolekcji, czy też nie:



Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace RadioButtonsExample.Models
{
    public class Helpers
    {
        public Dictionary<string, string> GetKeyValueFormControls(FormCollection formCollection)
        {
            Dictionary<string, string> result = new Dictionary<string, string>();
            if (formCollection != null && formCollection.Count > 0)
            {
                for (int i = 0; i < formCollection.AllKeys.Length; i++)
                {
                    result.Add(formCollection.AllKeys[i], formCollection[formCollection.AllKeys[i]]);
                }
            }

            return result;
        }
    }
}


Sposób II

Tym razem wykorzystamy możliwości bindingu wbudowane w MVC 2. Większość baz na rynku (zwłaszcza darmowych) nie posiada specjalnego pola pozwalającego zakwalifikować odpowiedź jako typ logiczny. Dwoma najprostszymi więc sposobami na uzyskanie zapisania do bazy odpowiedniej wartości takiego pola jest użycie zmiennej tekstowej typu CHAR(1) - reprezentującej zamknięty zbiór odpowiedzi {T,N} bądź też typ całkowity INTEGER o wartościach ze zbioru {0,1}. W moim przypadku wybiorę typ tekstowy.
Aby dodatkowo uprościć całość do klasy dołożę jedno dodatkowe pole, określające odpowiedź na zadane pytanie, oprócz tego utworzymy klasę Ankieta zawierającą listę pytań (tym razem w narodowym języku):



public class Pytanie
    {
        public int Id { get; set; }
        public string Tresc { get; set; }
        public string Odpowiedz { get; set; }
    }



using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace MvcApplication1.Models
{
    public class Ankieta
    {
        public List<Pytanie> Pytania { get; set; }
    }
}

Dzięki temu będzie można bezpośrednio z formularza zwrócić odpowiednią wartość. Oczywiście można by stworzyć oddzielną klasę na odpowiedź, w tym prostym przypadku nie skorzystam z tej możliwości.
Przejdźmy do konstrukcji widoku dla takiego modelu. Tutaj także posłużę się dwoma przykładami widoków. Pierwszy, aby pokazać co tak naprawdę dzieje się podczas bindowania kolekcji w MVC a drugi - oczywiście upraszczający całość:
1. Sposób trudniejszy, czyli co w środku piszczy:



Code:
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<MvcApplication1.Models.Ankieta>" %>

<!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>Index</title>
</head>
<body>
    <div>
        <% using (Html.BeginForm())
           { %>
           <%  for(int i = 0; i < Model.Pytania.Count; i++)
              { %>
              <%: Html.Hidden(string.Format("Pytania[{0}].ID", i), Model.Pytania[i].Id) %>
              <%: Html.DisplayFor(x => x.Pytania[i  ].Tresc) %>
              <br />
              <%: Html.RadioButton(string.Format("Pytania[{0}].Odpowiedz", i), "T") %>TAK
              <br />
              <%: Html.RadioButton(string.Format("Pytania[{0}].Odpowiedz", i), "N") %>NIE
              <br />
              <br />
           <% } %>

           <input type="submit" value="Wyślij" />
        <% } %>
    </div>
</body>
</html>

Sprawdźmy od razu co generuje ten kod w HTML:



Code:
<!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><title>
 Index
</title></head>
<body>
    <div>
        <form action="/" method="post"><input id="Pytania_0__ID" name="Pytania[0].ID" type="hidden" value="1" />
              Czy lubisz te strone?
              <br />
              <input id="Pytania_0__Odpowiedz" name="Pytania[0].Odpowiedz" type="radio" value="T" />TAK
              <br />
              <input id="Pytania_0__Odpowiedz" name="Pytania[0].Odpowiedz" type="radio" value="N" />NIE
              <br />
              <br />
           <input id="Pytania_1__ID" name="Pytania[1].ID" type="hidden" value="2" />
              Czy chcialbys pomoz tworzyc te strone?
              <br />
              <input id="Pytania_1__Odpowiedz" name="Pytania[1].Odpowiedz" type="radio" value="T" />TAK
              <br />
              <input id="Pytania_1__Odpowiedz" name="Pytania[1].Odpowiedz" type="radio" value="N" />NIE
              <br />
              <br />
           

           <input type="submit" value="Wyślij" />
        </form>
    </div>
</body>
</html>

Zwróćmy uwagę na tworzenie nazwy kontrolki. Mechanizm Bindingu w MVC nie tylko musi znać dokładną nazwę pola do którego binduje dany element strony, ale także pozycję w kolekcji. Dlatego też aby zwrócić poprawnie dane z formularza jako obiekt Ankieta:


Code:
<input id="Pytania_1__Odpowiedz" name="Pytania[1].Odpowiedz" type="radio" value="T" />TAK

Sama nazwa generowana jest w prosty sposób:


Code:
<%: Html.RadioButton(string.Format("Pytania[{0}].Odpowiedz", i), "T") %>TAK

Co prawda nie jest problemem pisanie takiego kodu, jednak po pewnym czasie staje się on mało czytelny (jak również dla kolejnych programistów tego projektu z pewnością nie będzie wygodnie domyślać się o co chodziło autorowi). Można całość uprościć do wyrażenia lambda:


Code:
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<MvcApplication1.Models.Ankieta>" %>

<!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>Index</title>
</head>
<body>
    <div>
        <% using (Html.BeginForm())
           { %>
           <%  for(int i = 0; i < Model.Pytania.Count; i++)
              { %>
              <%: Html.HiddenFor(x => x.Pytania[i].Id) %>.
              <%: Html.DisplayFor(x => x.Pytania[i].Tresc) %>
              <br />
              <%: Html.RadioButtonFor(x => Model.Pytania[i].Odpowiedz, "T")  %>TAK
              <br />
              <%: Html.RadioButtonFor(x => Model.Pytania[i].Odpowiedz, "N") %>NIE
              <br />
              <br />
           <% } %>

           <input type="submit" value="Wyślij" />
        <% } %>
    </div>
</body>
</html>

Oczywiście wygenerowany kawałek kodu HTML nie będzie się niczym różnił od poprzedniego, ale nie zawiera elementów gimnastyki dla zaawansowanych podczas tworzenia nazwy elementu HTML.

Sposób którzy obierzemy zależy oczywiście tylko od naszych preferencji. Ja przeważnie stosuje drugi, wykorzystujący binding. Uważam, że jest to bardziej poprawny sposób bindowania danych w frameworku, który właściwie słynie z łatwości bidningu całych obiektów do formularzy HTML.

Ktoś dociekliwy może posunąć się dalej i stworzyć własny HTML helper, generujący odpowienie bloki przycisków typu radio z odpowiednimi wartościami korzystając z list, typów wyliczeniowych itp. Ograniczeniem w tym przypadku jest tylko i wyłączenie wyobraźnia.

Brak komentarzy:

Prześlij komentarz