Pokazywanie postów oznaczonych etykietą ASP.NET MVC 2. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą ASP.NET MVC 2. Pokaż wszystkie posty

niedziela, 7 października 2012

Tworzenie i zwalnianie DbContext w aplikacji ASP.NET MVC

Korzystając z aplikacji ASP.NET MVC i EntityFrameworka warto stworzyć uniwersalny kod pobierania i zwalniania DbContex.

Przejdę od razu do implementacji:
Code:
    public static class MvcContext
    {
        public const string ContextName = "DatabaseContext";

        public static DatabaseContext Current
        {
            get
            {
                if (HttpContext.Current.Items[ContextName] == null)
                {
                    HttpContext.Current.Items[ContextName] = new DatabaseContext();
                }

                return (DatabaseContext)HttpContext.Current.Items[ContextName];
            }
        }
    }

Plik Global.asax:

Code:
public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
            AuthConfig.RegisterAuth();
        }

        protected void Application_EndRequest()
        {
            var entityContext = HttpContext.Current.Items[MvcContext.ContextName] as DatabaseContext;
            if (entityContext != null)
            {
                entityContext.Dispose();
            }
        }

        protected void Application_BeginRequest()
        {

        }
    }


Pytanie odnośnie kodu powyżej może być jedno: dlaczego DbContext nie jest tworzony w Application_BeginRequest() ?

Metody Application_BeginRequest oraz Application_EndRequest wywoływane są przy każdym żądaniu. Jeżeli weźmiemy stronę która ma 30 obrazków - zostałoby stworzonych 30 DbContext-tów.

W przypadku powyższego kodu DbContext zostanie stworzony tylko wtedy kiedy będzie potrzebny a zwolniony po zakończeniu żądania.

wtorek, 2 października 2012

Szyfrowanie Web.config

Szyfrowanie Web.config-a jest rzeczą oczywistą. Znajdują się w nim dane dostępowe do bazy danych, klucze do webserwisów oraz inne poufne dane.

Instrukcja szyfrowania Web.config-a:
1. Eksportujemy klucz maszyny (Machine key):

Code:
C:\Windows\Microsoft.NET\Framework\v4.0.30319\aspnet_regiis.exe -px "iisConf
igurationKey" C:\folder\iisConfKey.xml -pri

Code:
C:\Windows\Microsoft.NET\Framework\v4.0.30319\aspnet_regiis.exe -px "iisWasKey" C:\folder\iisWasKey.xml -pri


Wyeksportowany klucze znajdziemy pod ścieżką:

Code:
 C:\folder\



2. Import klucza na serwerach

Code:
c:\windows\Microsoft.NET\Framework64\v2.0.50727\aspnet_regiis.exe -pi "iisConfigurationKey" c:\folder\iisconfkey.xml -exp

Code:
c:\windows\Microsoft.NET\Framework64\v2.0.50727\aspnet_regiis.exe -pi "iisWasKey" c:\folder\iisWasKey.xml -exp


3. Autoryzacja zaimportowanego klucza

Code:
C:\Windows\Microsoft.NET\Framework\v4.0.30319\aspnet_regiis -pa 
"iisConfKey"


4. Do Web.config-a dodajemy sekcję zawierającą informacje nt używanego prowidera szyfrowania:
 Jest to krok konieczny jeżeli zamierzamy używać zaimportowanego klucza na wielu serwerach:

Code:
<configprotecteddata>
<providers>
<add keyContainerName=”MyWebServerRSA”
description=”Uses RsaCryptoServiceProvider to encrypt and decrypt”
name=”MyWebServerRSAProvider”
type=”System.Configuration.RsaProtectedConfigurationProvider,System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a” />
</providers>
</configProtectedData>


5. Przykładowe szyfrowanie sekcji connectionstringów:

Code:
C:\Windows\Microsoft.NET\Framework\v4.0.30319\aspnet_regiis -pef "connection
Strings" "C:\inetpub\WebApplication1" -prov "MyWebServerRSAProvider"

Po zakodowaniu sekcja connectionStrings będzie wyglądała w następujący sposób:

Code:
    <connectionStrings configProtectionProvider="MyWebServerRSAProvider">
        <EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element"
            xmlns="http://www.w3.org/2001/04/xmlenc#">
            <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" />
            <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
                <EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">
                    <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" />
                    <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
                        <KeyName>Rsa Key</KeyName>
                    </KeyInfo>
                    <CipherData>
                        <CipherValue>f19a64s6YB208ibupTTE6+8zWI3y+trQTAajwtpaoArJ8kLic2k0ifmmSmok3NU2Aw7vBUcR2oayzUsoJSuYIDePVr/zvI9/oMVuRMBE+nvzi598BlpZ3lxsggKiwrCO1U/h/zcgNQFuosu7PnX4Rz+4cXZB60LngaZe+Gk+jy8=</CipherValue>
                    </CipherData>
                </EncryptedKey>
            </KeyInfo>
            <CipherData>
                <CipherValue>6xZXUTJzr/ovYhI4qvw06hBA9qj59AKiOpEuwdx175R8HCiY/toW4G31GZwI237cmNTMPdlJac/VGki9C/UhU6Ehwb7VQoM8LjiqiJnWNkjyOUJYJOa+uZhpoVtEY06trjMqcFtLP/hdNiDtOwvOxusSFxGioP4Jwvfl22qTSBuc4j1QS1t8PSr/5tlZFJkSV6rxflMPJ+4Z4FYTHdfCyuk3vdnbO2RKjHyx3kNqjEz1eeKrKfz2jH9b6pbMKNss91+fE/ERjeGgdaP8wmewCw67zfjfx7B9fz/FZCw5MK+L2rUsnQMjqXCQEodVBbQtrDSGIlIAcQXGng8h2F9U09AzDSyZRrY+</CipherValue>
            </CipherData>
        </EncryptedData>
    </connectionStrings>

niedziela, 8 kwietnia 2012

Custom RoleProvider ASP.NET

Zabrałem się za projekt w ASP.NET MVC 3. W planach jest napisanie nowego, rewolucyjnego systemu :) można więc spodziewać się wpisów związanych z tą technologią.
Problem: w projekcie będzie możliwość zakładania kont użytkowników. Będzie także podział na role. Początkowo myślałem o wykorzystaniu gotowego rozwiązania (ASP.NET providers), jednak nie do końca mi się ono podoba. MembershipProvider i RoleProvider to świetne mechanizmy, ale tworzą w bazie standardowo niepotrzebny narzut zbędnych kolumn. Dlatego wziąłem się za pisanie ich pod mój system od początku.
O tworzeniu MembershipProvidera pisałem już we wcześniejszym poście, w tym trochę porad jak napisać RoleProvidera.

Sprawa jest łatwiejsza niż w przypadku MembershipProvidera, ze względu na to iż ilość metod które należy zaimplementować jest dużo mniejsza.

Startując od początku:

W bazie potrzebujemy odpowiednich tabel do przetrzymywania danych o użytkownikach i ich rolach:


Standardowa realizacja wiele do wiele (użytkownik może być przypisany do więcej niż jednej roli).

Wystartujemy od szablonu:

    public class MyRoleProvider : RoleProvider
    {
        public override bool IsUserInRole(string username, string roleName)
        {
            throw new NotImplementedException();
        }

        public override string[] GetRolesForUser(string username)
        {
            throw new NotImplementedException();
        }

        public override void CreateRole(string roleName)
        {
            throw new NotImplementedException();
        }

        public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
        {
            throw new NotImplementedException();
        }

        public override bool RoleExists(string roleName)
        {
            throw new NotImplementedException();
        }

        public override void AddUsersToRoles(string[] usernames, string[] roleNames)
        {
            throw new NotImplementedException();
        }

        public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
        {
            throw new NotImplementedException();
        }

        public override string[] GetUsersInRole(string roleName)
        {
            throw new NotImplementedException();
        }

        public override string[] GetAllRoles()
        {
            throw new NotImplementedException();
        }

        public override string[] FindUsersInRole(string roleName, string usernameToMatch)
        {
            throw new NotImplementedException();
        }

        public override string ApplicationName
        {
            get { throw new NotImplementedException(); }
            set { throw new NotImplementedException(); }
        }
    }

Zaczniemy od potrzebnych repozytoriów:

      using System.Linq;
using BusinessObjects.Entities;
using DataAccessLayer.Repositories.Contract;

namespace DataAccessLayer.Repositories.Implementation
{
    public class RoleRepository : BaseRepository, IRoleRepository
    {
        public void AddRole(Role role)
        {
            AddRole(role.Rolename);
        }

        public void AddRole(string roleName)
        {
            ChemistContext.Roles.Add(new Role{Rolename = roleName});
        }

        public IQueryable GetAllRoles()
        {
            return ChemistContext.Roles.AsQueryable();
        }

        public Role GetRoleByName(string roleName)
        {
            return ChemistContext.Roles.SingleOrDefault(x => x.Rolename == roleName);
        }

        public Role GetRoleById(int id)
        {
            return ChemistContext.Roles.SingleOrDefault(x => x.Id == id);
        }

        public IQueryable GetRolesForUser(string userName)
        {
            var userRoles = from user in ChemistContext.Users
                            join userInRoles in ChemistContext.UserInRoles on user.Id equals userInRoles.UserId
                            join roles in ChemistContext.Roles on userInRoles.RoleId equals roles.Id
                            where user.Login == userName
                            select roles;

            return userRoles;
        }

        public void DeleteRole(string roleName)
        {
            var role = ChemistContext.Roles.SingleOrDefault(x => x.Rolename == roleName);
            if (role != null)
            {
                ChemistContext.Roles.Remove(role);
                ChemistContext.SaveChanges();
            }
        }
    }
}

using System.Linq;
using BusinessObjects.Entities;
using DataAccessLayer.Repositories.Contract;

namespace DataAccessLayer.Repositories.Implementation
{
    public class UserInRoleRepository : BaseRepository, IUserInRoleRepository
    {
        public void AddUserToRole(UserInRole userInRole)
        {
            AddUserToRole(userInRole.UserId, userInRole.RoleId);
        }

        public void AddUserToRole(int idUser, int idRole)
        {
            ChemistContext.UserInRoles.Add(new UserInRole{UserId = idUser, RoleId = idRole});
            ChemistContext.SaveChanges();
        }

        public void AddUserToRole(string userName, string roleName)
        {
            var idUser = ChemistContext.Users.Single(x => x.Login == userName).Id;
            var idRole = ChemistContext.Roles.Single(x => x.Rolename == roleName).Id;
            AddUserToRole(idUser, idRole);
        }

        public IQueryable GetAll()
        {
            return ChemistContext.UserInRoles.AsQueryable();
        }

        public UserInRole GetByIdRoleIdUser(int idRole, int idUser)
        {
            return ChemistContext.UserInRoles.SingleOrDefault(x => x.RoleId == idRole && x.UserId == idUser);
        }

        public UserInRole GetByUserNameRoleName(string userName, string roleName)
        {
            var userInRole = from users in ChemistContext.Users
                             join userInRoles in ChemistContext.UserInRoles on users.Id equals userInRoles.UserId
                             join roles in ChemistContext.Roles on userInRoles.RoleId equals roles.Id
                             where users.Login == userName && roles.Rolename == roleName
                             select userInRoles;

            return userInRole.SingleOrDefault();
        }

        public void DeleteRoleAndUsers(string roleName)
        {
            var userRolesToDelete = from roles in ChemistContext.Roles
                                    join userInRoles in ChemistContext.UserInRoles on roles.Id equals userInRoles.RoleId
                                    where roles.Rolename == roleName
                                    select userInRoles;
            foreach (var userInRole in userRolesToDelete)
            {
                ChemistContext.UserInRoles.Remove(userInRole);
            }

            ChemistContext.SaveChanges();
        }

        public void DeleteUserFromRole(string userName, string roleName)
        {
            var usersInRole = from users in ChemistContext.Users
                              join userInRoles in ChemistContext.UserInRoles on users.Id equals userInRoles.UserId
                              join roles in ChemistContext.Roles on userInRoles.RoleId equals roles.Id
                              where roles.Rolename == roleName && users.Login == userName
                              select userInRoles;

            foreach (var user in usersInRole)
            {
                ChemistContext.UserInRoles.Remove(user);
            }

            ChemistContext.SaveChanges();
        }

        public string[] GetByRoleName(string roleName)
        {
            var usersInRole = from users in ChemistContext.Users
                              join userInRoles in ChemistContext.UserInRoles on users.Id equals userInRoles.RoleId
                              join roles in ChemistContext.Roles on userInRoles.RoleId equals  roles.Id
                              where roles.Rolename == roleName
                              select users.Login;

            return usersInRole.ToArray();
        }

        public string[] GetByRoleNameContains(string roleName)
        {
            var usersInRole = from users in ChemistContext.Users
                              join userInRoles in ChemistContext.UserInRoles on users.Id equals userInRoles.RoleId
                              join roles in ChemistContext.Roles on userInRoles.RoleId equals roles.Id
                              where roles.Rolename.Contains(roleName)
                              select users.Login;

            return usersInRole.ToArray();
        }
    }
}

Nie pozostaje nic innego jak zaimplementowanie samego RoleProvidera:

using System;
using System.Configuration.Provider;
using System.Transactions;
using System.Web.Security;
using BusinessObjects.Enums;
using DataAccessLayer.Repositories.Contract;
using DataAccessLayer.Repositories.Implementation;
using System.Linq;
using Services.Contract;
using Services.Implementation;

namespace Chemist.Infrastructure
{
    public class ChemistRoleProvider : RoleProvider
    {
        private readonly IRoleRepository roleRepository;
        private readonly IUserInRoleRepository userInRoleRepository;
        private readonly IExceptionService exceptionService = new ExceptionService(new EventLogRepository());
        private string applicationName = "Chemistry";

        public ChemistRoleProvider()
        {
            roleRepository = new RoleRepository();
            userInRoleRepository = new UserInRoleRepository();
        }

        public override bool IsUserInRole(string userName, string roleName)
        {
            var isUserInRole = userInRoleRepository.GetByUserNameRoleName(userName, roleName) != null;

            return isUserInRole;
        }

        public override string[] GetRolesForUser(string userName)
        {
            return roleRepository.GetRolesForUser(userName).Select(x => x.Rolename).ToArray();
        }

        public override void CreateRole(string roleName)
        {
            roleRepository.AddRole(roleName);
        }

        public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
        {
            if (!RoleExists(roleName))
            {
                throw new ProviderException("Role does not exist.");
            }
            if (throwOnPopulatedRole && GetUsersInRole(roleName).Length > 0)
            {
                throw new ProviderException("Cannot delete a populated role.");
            }

            try
            {
                using (
                    var transaction = new TransactionScope(TransactionScopeOption.Required,
                                                           new TransactionOptions
                                                           {IsolationLevel = IsolationLevel.ReadCommitted}))
                {
                    userInRoleRepository.DeleteRoleAndUsers(roleName);
                    roleRepository.DeleteRole(roleName);
                    transaction.Complete();
                    return true;
                }
            }
            catch (Exception ex)
            {
                exceptionService.Write(ex, EventSource.RoleProvider);
                return false;
            }
        }

        public override bool RoleExists(string roleName)
        {
            var roleExist = roleRepository.GetRoleByName(roleName);

            return roleExist != null;
        }

        public override void AddUsersToRoles(string[] userNames, string[] roleNames)
        {
            foreach (var rolename in roleNames)
            {
                if (!RoleExists(rolename))
                {
                    throw new ProviderException("Role name not found.");
                }
            }

            foreach (var username in userNames)
            {
                if (username.Contains(","))
                {
                    throw new ArgumentException("User names cannot contain commas.");
                }

                foreach (var rolename in roleNames)
                {
                    if (IsUserInRole(username, rolename))
                    {
                        throw new ProviderException("User is already in role.");
                    }
                }
            }

            using (var transaction = new TransactionScope())
            {
                foreach (var username in userNames)
                {
                    foreach (var rolename in roleNames)
                    {
                        userInRoleRepository.AddUserToRole(username, rolename);
                    }
                }

                transaction.Complete();
            }
        }

        public override void RemoveUsersFromRoles(string[] userNames, string[] roleNames)
        {
            foreach (var rolename in roleNames)
            {
                if (!RoleExists(rolename))
                {
                    throw new ProviderException("Role name not found.");
                }
            }

            foreach (var username in userNames)
            {
                foreach (var rolename in roleNames)
                {
                    if (!IsUserInRole(username, rolename))
                    {
                        throw new ProviderException("User is not in role.");
                    }
                }
            }

            foreach (string username in userNames)
            {
                foreach (string rolename in roleNames)
                {
                    userInRoleRepository.DeleteUserFromRole(username, rolename);
                }
            }
        }

        public override string[] GetUsersInRole(string roleName)
        {
            return userInRoleRepository.GetByRoleName(roleName);
        }

        public override string[] GetAllRoles()
        {
            return roleRepository.GetAllRoles().Select(x => x.Rolename).ToArray();
        }

        public override string[] FindUsersInRole(string roleName, string usernameToMatch)
        {
            return userInRoleRepository.GetByRoleNameContains(roleName);
        }

        public override string ApplicationName
        {
            get { return applicationName; }
            set { applicationName = value; }
        }
    }
}

Ostatnim krokiem, który należy wykonać, to dodanie w Web.configu informacji o tym, aby został użyty nasz RoleProvider:


Code:
    <roleManager enabled="true" defaultProvider="ChemistRoleProvider">
      <providers>
        <add name="ChemistRoleProvider" type="Chemist.Infrastructure.ChemistRoleProvider"/>
      </providers>
    </roleManager>



Ilość kodu może wydać się duża, jednak raz napisany RoleProvider jest przeważnie wykorzystywany w wielu późniejszych projektach.

poniedziałek, 29 sierpnia 2011

jqPlot - wykresy w ASP MVC

Chcąc wyświetlić na naszej stronie wykres wartości, możemy skorzystać z Silverlighta lub javascriptu. Możliwści silvrlighta podczas tworzenia wykresów prezentowałem w jednym z wcześniejszych postów. Teraz pokażę w jaki sposób można za pomocą JSONa i biblioteki jQPlot wyświetlić wykres dla pewnych wartości (np. pobranych z bazy danych).
Ze strony http://www.jqplot.com/ pobieramy bibliotekę jQPlot.
Po pobraniu dodajemy pliki pluginu do naszego projektu:


Następnie dodajemy odnośniki do bibliotek jQuery oraz jQPlot:


Code:
<script src="../../Scripts/jquery-1.4.1.min.js" type="text/javascript"></script>
    <script src="../../Scripts/jquery.jqplot.min.js" type="text/javascript"></script>
    <link href="../../Scripts/jquery.jqplot.css" rel="stylesheet" type="text/css" />

W naszym widoku umieścimy kod znajdujący się na stronie autora dodatku:


Code:
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
 
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Home Page
</asp:Content>
 
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <h2><%: ViewData["Message"] %></h2>
    <div id="chartdiv" style="height:400px;width:300px; "></div>
    <script type="text/javascript">
        $.jqplot('chartdiv', [[[1, 2], [3, 5.12], [5, 13.1], [7, 33.6], [9, 85.9], [11, 219.9]]]);
    </script>
</asp:Content>

Po uruchomieniu aplikacji otrzymamy następujący widok:



Oczywiście sztywne umieszczenie wartości w kodzie nie przyda się nam zbytnio. Potrzebna jest nam metoda, która pozwoli zwrócić dane do widoku:


Code:
public ActionResult GetChartData()
        {
            JsonResult jsonResult = new JsonResult();
            int[] data = { 1, 5, 6, 8, 3, 4 };
            jsonResult.Data = data;
            jsonResult.JsonRequestBehavior = JsonRequestBehavior.AllowGet;

            return jsonResult;
        }

W widoku umieszczamy kod jQuery odpowiedzialny za odebranie danych z kontrolera:


Code:
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Home Page
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <h2><%: ViewData["Message"] %></h2>
    <div id="chartdiv" style="height:400px;width:300px; "></div>
    <script type="text/javascript">
        $(document).ready(function () {
            $.getJSON("/Home/GetChartData", null, function (data) {
                $.jqplot('chartdiv', [data]);
            });
        });
    </script>
</asp:Content>

Po uruchomieniu aplikacji zobaczymy następujący wykres:


Oczywiście jqPlot oferuje dużo więcej możliwości, m.in.:
- różne typy wykresów
- osie z dowolnym formatowaniem
- automatyczne wyliczanie linii trendu
- podświetlanie punktów i wyświetlanie wartości w danym punkcie
- drag and drop punktów
To tylko kilka z wielu możliwości tego komponentu.
Zachęcam do używania i eksperymentowania.

JSON obsługa w ASP MVC 2

Z pewnością wielu z Was spotkało się z formatem JSON - JavaScript Object Notation. Format ten przekazywany jest w postaci tekstowej. Ponieważ dane przekazywane są jako zwykły tekst, mniej zajmują a co za tym idzie przesyłanie jest szybsze niż w przypadku standardowego XMLa. Obecnie wykorzystywany jest w aplikacjach silnie korzystających z AJAXu.
ASP MVC 2 pozwala w łatwy sposób na przesyłanie danych w omawianym formacie. Wystarczy tylko zwrócić dane w metodzie kontrolera jako typ JsonResult.

Zobaczmy na bardzo prosty przykład:
W kontrolerze umieścimy metodę zwracającą tablicę liczb od 1 do 20, a następnie stworzymy z niej listę w naszym widoku:


Code:
public ActionResult GetJSONData()
        {
            int[] data = Enumerable.Range(1, 20).ToArray();
            JsonResult jSonResult = new JsonResult();
            jSonResult.Data = data;
            jSonResult.JsonRequestBehavior = JsonRequestBehavior.AllowGet;

            return jSonResult;
        }

Widok Home:


Code:
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Home Page
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <h2><%: ViewData["Message"] %></h2>
    <br />

    <div id="JsonDiv">
        <ul>
            
        </ul>
    </div>

    <script type="text/javascript">
        $(document).ready(function () {
            $.getJSON("/Home/GetJSONData", null, function (data) {
                $.each(data, function (index, value) {
                    var newValue = "<li>" + value + "</li>";
                    $("#JsonDiv ul").append(newValue);
                });
            });
        });
    </script>
</asp:Content>

Parsowanie JSONa przy użyciu jQuery jest niezwykle proste. W tym przypadku:
  1. Czekamy aż dokument całkowicie się załaduje w celu dodania do diva listy naszych liczb
  2. Pobieramy dane przekazane przez metodę za pomocą metody getJSON - przekazując jako argument nazwę kontrolera i metody która zwraca dane
  3. Za pomocą funkcji each iterujemy po kolekcji zawierającej nasze dane i dodajemy je do listy
Rezultat:





Jak widać proste i bardzo łatwe, podobnie ma się rzecz do obiektów o bardziej skomplikowanej strukturze:

W modelu tworzymy definicję klasy Person:


Code:
public class Person
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateTime BirthDate { get; set; }
        public List<Car> Cars { get; set; }

        public Person(int id, string firstName, string lastName, DateTime birthDate)
        {
            Id = id;
            FirstName = firstName;
            LastName = lastName;
            BirthDate = birthDate;
        }
    }

Następnie klasę Car:


Code:
public class Car
    {
        public string Make { get; set; }
        public int Year { get; set; }
        public string Model { get; set; }
    }

Ostatnia klasa będzie klasą generującą testowe dane:


Code:
public class FakeData
    {
        public List<Person> GetFakeData(int fakeEntitiesCount)
        {
            List<Person> lPerson = new List<Person>();
            Random r = new Random();
            string[] firstNames = { "Jacek", "Maciek", "Paweł", "Sylwek" };
            string[] lastNames = { "Kowalski", "Markowski", "Piotrkowski" };
            string[] carMakers = { "VW", "Ford", "Fiat" };
            string[] carModels = { "Jetta", "Bora", "Focus", "Panda" };

            for (int i = 0; i < fakeEntitiesCount; i++)
            {
                Person p = new Person(i, firstNames[r.Next(0, firstNames.Length)], lastNames[r.Next(0, lastNames.Length)],
                    DateTime.Now.AddYears(r.Next(19, 50)));
                p.Cars = new List<Car>();
                int carCount = r.Next(0, 4);
                for (int j = 0; j < carCount; j++)
                {
			        p.Cars.Add(new Car { Make = carMakers[r.Next(0, carMakers.Length)], 
                        Model = carModels[r.Next(0, carModels.Length)],
                        Year = DateTime.Now.AddYears(r.Next(0,8)).Year});
                }
                lPerson.Add(p);
            }

            return lPerson;
        }
    }

Oczywiście testowe dane - to tylko testowe dany, raczej każdy wie że Fiat nie produkuje Bory :).

W metodzie Index naszego kontrolera w zmiennej sesji przechowamy dane odnośnie osób i ich samochodów:


Code:
public ActionResult Index()
        {
            Session["Data"] = new FakeData().GetFakeData(20);
            ViewData["Message"] = "Przekazywanie danych w formacie JSON";

            return View();
        }

Metoda CarEvidence zwróci nam listę osób:


Code:
public ActionResult CarEvidence()
        {
            List<Person> lPerson = new List<Person>();
            if (Session["Data"] != null)
            {
                lPerson = (List<Person>)Session["Data"];
                List<SelectListItem> lPersonList = new List<SelectListItem>();
                foreach (var person in lPerson)
                {
                    lPersonList.Add(new SelectListItem { Text = person.FirstName + " " + person.LastName, Value = person.Id.ToString() });
                }
                ViewData["PersonList"] = lPersonList;
            }

            return View(lPerson);
        }

Łatwo zauważyć, że jeżeli ktoś od razu wejdzie na stronę CarEvidence nie otrzyma żadnych danych - jest to jednak tylko przykład w realnej aplikacji z pewnością źródłem danych będzie baza danych bądź plik XML.

Za pomocą metody GetPersonCars wydobędziemy informacje o samochodach danego kierowcy:


Code:
[HttpGet]
        public ActionResult GetPersonCars(int idPerson)
        {
            JsonResult jSonData = new JsonResult();
            jSonData.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
            if (Session["Data"] != null)
            {
                List<Car> lCars = ((List<Person>)Session["Data"]).Where(x => x.Id == idPerson).SingleOrDefault().Cars;
                jSonData.Data = lCars;
            }

            return jSonData;
        }

Definicja widoku CarEvidence:


Code:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<IEnumerable<MvcApplication2.Models.Person>>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
	CarEvidence
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <h2>CarEvidence</h2>
    <div>
        <%: Html.Label("Wybierz osobę: ")%>
        <br />
        <%: Html.DropDownList("ddlPersonList", (List<SelectListItem>)ViewData["PersonList"]) %>
    </div>

    <div id="PersonCars">
        <ul>
        </ul>
    </div>

    
    <script type="text/javascript">
        $("#ddlPersonList").change(function () {
            var id = $("#ddlPersonList").val();
            $.getJSON("/Home/GetPersonCars/", { idPerson: $(this).val() }, function (data) {
                $("#PersonCars ul li").remove();
                $.each(data, function (index, value) {
                    var car = "<li>" + value.Year + " " + value.Make + " " + value.Model + "</li>";
                    $("#PersonCars ul").append(car);
                });
            });
        });    
    </script>

</asp:Content>

Po uruchomieniu aplikacji otrzymamy następujący widok:





Zmieniając wybór na liście, możemy oglądać dane o samochodach danego użytkownika.
Kilka uwag co do kodu jQuery tutaj użytego:
- Metoda change przechwytuje zdarzenie zmiany elementu na liście
- funkcja val() pobiera wartość - id osoby danego elementu listy
- metoda getJSON pobiera JSON przesyłając do metody kontrolera informację o wybranej osobie: {idPerson: $(this).val()}
- za pomocą metody each()  - wyświetlamy informacje o samochodach wybranego kierowcy

Jak widać korzystanie z JSONa nie jest trudne. Można za pomocą niego uzyskać bardzo ciekawe efekty bez potrzeby przeładowywania całej strony. System serializacji wbudowany w ASP MVC z łatwością deserializuje i serializuje dane przekazywane w tym formacie - co czyni jego użycie jeszcze prostszym. jQuery dopełnia zestaw narzędzi pozwalając w prosty sposób wyświetlić użytkownikowi otrzymane dane czy też przekazać je do kontrolera w celu otrzymania żądanych dnaych.

Miłego eksperymentowania.

niedziela, 15 maja 2011

Wilojęzyczność w ASP.NET MVC

Kiedyś pisałem o globalizacji w ASP.NET WebForms (link). Jako, że ostatnimi czasy bardziej interesuje się MVC postanowiłem przenieść tamtą idę do nowego środowiska.

Na początek ustawmy środowisko testowe naszej aplikacji.
Jako przeglądarki użyję IE9. Aby zmienić w niej wyświetlany język wybieramy kolejno opcje internetowe a następnie na zakładce Ogólne w dziale Wygląd opcje Języki. Ja dodałem tutaj język angielski:



Należy pamiętać, że kolejność języków ma znaczenie. Najwyżej powinien się znajdować język preferowany lub jeżeli użytkownik nie ustawił innego, język który jest używany lokalnie w danym rejonie.

Aby odczytać język używany przez przeglądarkę użytkownika wystarczy zastosować prostą konstrukcję:


Code:
string[] languages = Request.UserLanguages;

Warto zauważyć tutaj ciekawą rzecz a mianowicie:


W niektórych przypadkach otrzymujemy tzw. kwalifikator (q=0.5) tak więc próbując przesłać takie dane np. do konstruktora CultureInfo() otrzymamy wyjątek. Dlatego powyższa linijka powinna wyglądać w następujący sposób:


Code:
string mainLanguage = Request.UserLanguages[0].Split(';')[0];

Mając informacje odnośnie preferowanego przez użytkownika języka, możemy przystąpić do projektowania interfejsu użytkownika.

Wielojęzyczność można osiągnąć na kilka sposobów. W zależności czy mamy do czynienia z statyczną stroną w której treść się nie zmienia czy też jest to strona na której często dodajemy newsy wybierzemy odpowiedni sposób. Jeżeli mamy do czynienia z stroną statyczną możemy skorzystać z resourców (zasobów). Jeżeli korzystamy ze strony na której często zmienia się treść (np. blogu) w takim przypadku należy przygotować odpowiednią strukturę w bazie danych, która pozwoli na przechowywanie artykułów w kilku językach.

Przedstawię tutaj prostszą wersję nadającą się świetnie dla prostych, statycznych stron.

W celach demonstracyjnych stworzymy prosty projekt zawierający jedną stronę oraz MasterPage na którym umieścimy powitanie na stronie w odpowiednim języku. Nasz projekt będzie mieć następującą strukturę:


Projekt jest więc bardzo prosty. Aby skorzystać z dobrodziejstw zasobów należy stworzyć strukturę odpowiadającą obecnej strukturze:


Po wypełnieniu wartościami kopiujemy nasz zasób i tworzymy odpowiednie nazwy związane z nazwą języka:


Dla każdego z języków powtarzamy taką samą operację.

Teraz przejdziemy do wyświetlenia odpowiedniej informacji na stronie w odpowiednim języku. W naszym MasterPage-u zmieniamy zawartość z:


Code:
<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>

<!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><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
</head>
<body>
    <div>
        <h1>Witaj na stronie</h1>
    </div>
    <div>
        <asp:ContentPlaceHolder ID="MainContent" runat="server">
        
        </asp:ContentPlaceHolder>
    </div>
</body>
</html>

na:


Code:
<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>

<!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><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
</head>
<body>
    <div>
        <h1><%: MvcApplication3.Resources.Shared.MasterPage.Greatings %></h1>
    </div>
    <div>
        <asp:ContentPlaceHolder ID="MainContent" runat="server">
        
        </asp:ContentPlaceHolder>
    </div>
</body>
</html>

Następnie należy wczytać odpowiedni język ustawiony w przeglądarce użytkownika. W tym celu w pliku Global.asax dodajemy następującą metodę:


Code:
protected void Application_BeginRequest(object sender, EventArgs e)
        {
            if (Request.UserLanguages.Length > 0)
            {
                string language = Request.UserLanguages[0].Split(';')[0];
                if (language.Contains("en"))
                {
                    Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture("en");       
                }
            }
        }

Po tym zabiegu nasza strona w zależności od ustawień przeglądarki użytkownika będzie wyświetlała powitanie w języku polskim bądź też angielskim.
Rozwiązanie nadaje się do wyświetlania stałych elementów strony. Jak wspominałem wcześniej do wyświetlania artykułów należy skorzystać z dobrze zaprojektowanej bazy danych.

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.