ASP.NET MVC – typické postupy

V tomto článku bych rád ukázal, jak v ASP.NET MVC implementovat typické postupy z webových aplikací, tedy zobrazit seznam produktů, přidat nový produkt, editovat existující produkt, smazat produkt a umožnit dělat některé věci jen oprávněným uživatelům. Udělal jsem jednoduchou aplikaci, kterou si můžete stáhnout zde – využívá ASP.NET MVC Beta a ASP.NET MVC Beta Futures. Oboje je ale z projektu referencováno s CopyLocal na true, takže všechny potřebné assemblies jsou přímo u projektu.

Struktura projektu

Ve složce Controllers mám jeden kontroler nazvaný HomeController, do kterého jsem umístil všechny akce. Tento kontroler je odvozen od třídy CustomController, která obsahuje záležitosti, které bychom mohli využít v případných dalších kontrolerech v projektu.

Složka Views obsahuje jednotlivé pohledy aplikace, tedy všechny stránky, které budeme zobrazovat (všechny jsou založené na master page). Pohled Index zobrazuje seznam produktů, Categories seznam kategorií, ProductDetails slouží k vytvoření a úpravě produktu, Login k zadání přihlašovacích údajů a konečně Error je stránka, která se zobrazí při chybě.

Ve složce Models je pak všechno ostatní 😉 Třída BusinessLayer nečekaně představuje business vrstvu, tedy logiku celé aplikace – v reálné větší aplikaci by zde něco takového vůbec nebylo a roli této třídy by převzal samostatný projekt. Třída Product představuje produkt, Category kategorii, LoggedUser zalogovaného uživatele. Soubor Models.cs obsahuje modely – tedy třídy sloužící k předání dat z kontroleru do pohledu (a příp. obráceně). A konečně soubor ControllerAspects.cs obsahuje aspekty aplikované na metody kontrolerů.

Průřezové koncerny

Nejprve se podíváme na záležitosti, které prorůstají celou aplikací (což jsou věci přímo stvořené k obhospodaření pomocí AOP). Aspekty společné pro všechny akce jsem implementoval přímo v bázové třídě CustomController:

[HandleError]
public class CustomController : System.Web.Mvc.Controller
{

    public SimpleMVCApplication.Models.LoggedUser LoggedUser
    {
        get;
        set;
    }

    protected override void OnAuthorization(AuthorizationContext filterContext)
    {
        if (this.ViewData != null && this.HttpContext != null && this.HttpContext.Session != null)
        {
            this.ViewData["user"] = this.LoggedUser = this.HttpContext.Session["user"] as SimpleMVCApplication.Models.LoggedUser;
        }
    }

    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        string m;
        if (this.Request != null && this.Request.Params != null && this.ViewData != null && !string.IsNullOrEmpty(m = this.Request.Params["message"]))
        {
            this.ViewData["message"] = m;
        }
    }

    protected override void OnException(System.Web.Mvc.ExceptionContext filterContext)
    {
        if (filterContext != null && filterContext.Exception != null && this.ViewData != null && this.ViewData.ModelState != null)
        {
            this.ViewData.ModelState.AddModelError("global", filterContext.Exception.Message ?? filterContext.Exception.ToString());
            filterContext.Result = this.View();
            filterContext.ExceptionHandled = true;
        }
    }

} 

Property LoggedUser představuje zalogovaného uživatele, pro kterého se provádí tento požadavek. Třída LoggedUser je velmi jednoduchá:

public class LoggedUser
{

    public string Name
    {
        get;
        set;
    }

    public int ID
    {
        get;
        set;
    }

} 

V metodě OnAuthorization třídy CustomController zkusíme načíst údaj o zalogovaném uživateli ze session, kam jsme si tento údaj uložili při zalogování uživatele. Tato metoda je volána před každou akcí. Hned po ní je volána metoda OnActionExecuting, ve které provedeme načtení položky message z „nějakých“ parametrů a uložíme ji do pole ViewData. Tuto hodnotu si pak přečte master page a zobrazí ji. Toho využívám k předávání zprávy mezi požadavky – např. má dojít k přesměrování na akci Index, protože nebyl nalezen produkt k editaci, tak akce provádějící směrování uloží zprávu např. do GET parametru. No a konečně metoda OnException udělá to, že uloží do kolekce ViewData.ModelState údaj o vyvolané výjimce – tato metoda je totiž volána vždy když dojde k neošetřené výjimce.

Aspekty, které jsou specifické pro jednotlivé akce jsou umístěny v souboru ControllerAspects.cs a jsou pouze dva:

public class PageTitleAttribute : System.Web.Mvc.ActionFilterAttribute
{
    public PageTitleAttribute(string name)
    {
        this.Name = name;
    }

    public string Name
    {
        get;
        protected set;
    }

    public override void OnActionExecuting(System.Web.Mvc.ActionExecutingContext filterContext)
    {
        if (filterContext != null && filterContext.Controller != null && filterContext.Controller.ViewData != null)
        {
            filterContext.Controller.ViewData["Title"] = this.Name;
        }
    }
} 

Tento aspekt je volán před vykonáním akce a slouží k tomu, aby uložil do ViewData záznam o názvu stránky, který se pak použije v master page pro nastavení titulku stránky a vykreslení nadpisu. Druhý aspekt se stará o to, aby se k dané akci dostali pouze zalogovaní uživatelé. Pokud není uživatel zalogován, tak přeruší vykonávání akce (resp. k němu vůbec nedojde) a provede přesměrování:

public class AuthorizeAttribute : System.Web.Mvc.FilterAttribute, System.Web.Mvc.IAuthorizationFilter
{

    public AuthorizeAttribute()
    {
        this.LoginController = "Home";
        this.LoginAction = "Login";
    }

    public string LoginController
    {
        get;
        set;
    }

    public string LoginAction
    {
        get;
        set;
    }

    public void OnAuthorization(System.Web.Mvc.AuthorizationContext filterContext)
    {
        if (filterContext != null && filterContext.Controller is SimpleMVCApplication.Controllers.CustomController)
        {
            var controller = (CustomController)filterContext.Controller;
            if (controller.LoggedUser == null)
            {
                filterContext.Cancel = true;
                filterContext.Result = new System.Web.Mvc.RedirectToRouteResult(
                    new System.Web.Routing.RouteValueDictionary(new { controller = this.LoginController, action = this.LoginAction }));
            }
        }
    }

} 

Zde je důležité se uvědomit, že vždy dojde nejprve ke spuštění aspektu přímo v kontroleru a pak až dalších dodaných přes atributy. Díky tomu zde můžeme předpokládat, že již máme nastavenu property LoggedUser.

Logování

Při logování používám pro předání z kontroleru do pohledu třídu LoginModel:

public class LoginModel
{
    public string UserName
    {
        get;
        set;
    }

    public string Password
    {
        get;
        set;
    }

    public bool Remember
    {
        get;
        set;
    }
} 

Po přijetí požadavku na stránku Home/Login je vyvolána tato akce:

[AcceptVerbs(HttpVerbs.Get), PageTitle("Login")]
public ActionResult Login()
{
    return View(new Models.LoginModel());
}

Atribut AcceptVerbs nám zajistí, že tato akce bude zpracována jen v případě GET požadavku na tuto stránku a pomocí našeho aspektu PageTitle nastavíme titulek stránky. Dále pak pouze zavoláme metodu View s prázdným modelem, která způsobí použití pohledu Home/Login. Pokud bychom chtěli mít při prvním zobrazení logovacího pohledu předvyplněné nějaké hodnoty, stačilo by u modelu nastavit odpovídající properties.

Pohled Home/Login reprezentovaný stránkou Login.aspx vypadá takto:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="SimpleMVCApplication.Views.Home.Login" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">

    <% using(Html.BeginForm<HomeController>(c => c.Login(), FormMethod.Post))
       {
    %>
            <table class="table-centered">
                <tr>
                    <td>Username:</td>
                    <td><%= Html.TextBox("model.UserName")%></td>
                </tr>
                <tr>
                    <td>Password:</td>
                    <td><%= Html.Password("model.Password")%></td>
                </tr>
                <tr>
                    <td></td>
                    <td>Remember me <%= Html.CheckBox("model.Remember", true)%></td>
                </tr>
                <tr>
                    <td></td>
                    <td><%= Html.SubmitButton("submit", "Login") %></td>
                </tr>
            </table>
    <%
       }
    %>

</asp:Content> 

Jedná se tedy o klasický mix C# a HTML. Pomocí Html.BeginForm vyrenderujeme tag pro webový formulář, přičemž akci zavolanou pro obsloužení formuláře zde specifikujeme silně typově – pomocí lambda funkce. Metody property Html pak použijeme pro vykreslení hodnot modelu a submitovacího tlačítka. Pokud bychom hodnoty modelu jen zobrazovaly, nemuseli bychom hodnoty pro vykreslení prefixovat hodnotou „.model“ – toto prefixování je ale nutné pro správné předání hodnot do obsluhující akce. Hodnoty modelu bychom mohli také vyrenderovat přímo použitím property ViewData.Model, která je typu LoginModel díky tomuto kódu v souboru Login.aspx.cs:

public partial class Login : ViewPage<SimpleMVCApplication.Models.LoginModel>
{
} 

Akce zpracovávající data z logovacího formuláře vypadá takto:

[AcceptVerbs(HttpVerbs.Post), PageTitle("Login")]
public ActionResult Login(Models.LoginModel model)
{
    using (var bl = new BusinessLayer())
    {
        Models.LoggedUser user = bl.CheckLogin(model.UserName, model.Password);
        if (user != null)
        {
            this.HttpContext.Session.Add("user", user);
            return RedirectToAction("Index", new { message = "Logged in!" });
        }
        else
        {
            this.ViewData.ModelState.AddModelError("login", "Login invalid.");
            return View(model);
        }
    }
}

Zde máme akci, jejíž parametr není primitivní typ, ale třída, a tudíž musíme určit, jakým způsobem provedeme mapování hodnot z formuláře na položky této třídy – proto jsme v Login.aspx použili prefix „.model„. Tento prefix ale můžeme změnit atributem Bind.
Provede se ověření uživatele vůči business vrstvě a pokud je úspěšné, je do session uložena informace o úspěšném zalogování a požadavek je přesměrován na akci Index. V případě neúspěšného přihlášení je zobrazena tatéž stránka s tím samým modelem (takže zadaná data zůstanou zachována). Před tím je ale uložena chybová zpráva do ViewData.ModelState, která je pak vykreslena v master page pomocí metody Html.ValidationSummary(), která vykreslí všechny chyby uložené ve ViewData.ModelState.

Poslední akcí související s logováním je akce pro odlogování:

[PageTitle("Logout"), SimpleMVCApplication.Aspects.Authorize]
public ActionResult Logout()
{
    this.HttpContext.Session.Remove("user");
    return RedirectToAction("Index");
} 

Ta jen provede výmaz záznamu o zalogování ze session.

Seznam kategorií

Pro předání dat z kontroleru do pohledu pro zobrazení seznamu kategorií použijeme tento model (ze souboru Models.cs):

public class HomeCategoriesModel
{

    public List<Product> Products
    {
        get;
        set;
    }

    public List<Category> Categories
    {
        get;
        set;
    }

} 

Předáme si tedy seznam všech kategorií a také seznam všech produktů – to proto, že budeme chtít zobrazit počet produktů v každé kategorii. Pohled Categories.aspx, tedy stránka odvozená od WebPage<HomeCategoriesModel> (abychom měli property ViewData.Model typu HomeCategoriesModel), vypadá takto:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Categories.aspx.cs" Inherits="SimpleMVCApplication.Views.Home.Categories" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">

    <table class="table-centered" width="80%" border="1">
    
        <% bool logged = ViewData["user"] is LoggedUser; %>
        
        <tr><th>Category name</th><th>Products</th><% if (logged) { %><th>Actions</th><% } %></tr>
        
        <%  foreach (var category in ViewData.Model.Categories)
            {
        %>
                <tr>
                    <td><%= Html.Encode(category.Name) %></td>
                    <td><%= Html.Encode(ViewData.Model.Products.Count(p => p.CategoryID == category.ID).ToString()) %></td>
                    <%  if (logged)
                        {
                    %>
                            <td align="center" valign="middle"><%= Html.ActionLink<HomeController>(c => c.AddProduct(category.ID), "Add product") %></td>
                    <%
                        }
                    %>
                </tr>
        <%
            }
        %>
        
        
    </table>

</asp:Content> 

Nejprve si zjistíme, zda je uživatel zalogovaný, protože pro zalogované uživatele budeme kromě názvu kategorie a počtu produktů v kategorii zobrazovat také tlačítko, které slouží k přidání nového produktu do dané kategorie.
Data bychom neměli nikdy vykreslovat přímo, ale použít např. zde uvedenou metodu Html.Encode, která nám zajistí, že jsou ošklivé znaky (např. < nebo >) převedeny na neškodné HTML ekvivalenty. Pro vytvoření linku na akci používám metodu Html.ActionLink, která mi umožní odkazovanou akci specifikovat silně typově.

Akce pro zobrazení kategorie pak vypadá naprosto přímočaře – předá pohledu model, jehož položky naplní z business vsrtvy:

[PageTitle("Categories")]
public ActionResult Categories()
{
    using (var bl = new BusinessLayer())
    {
        return View(new HomeCategoriesModel { Categories = bl.Categories, Products = bl.Products });
    }
} 

Akce pro obsloužení odkazu na přidání produktu do dané kategorie vypadá takto:

[PageTitle("Add product"), SimpleMVCApplication.Aspects.Authorize]
public ActionResult AddProduct(int? id)
{
    using (var bl = new BusinessLayer())
    {
        return View("ProductDetails", new HomeProductDetailsModel
        {
            View = HomeProductDetailsModel.ViewType.Add,
            Categories = bl.Categories,
            Product = new Product
            {
                CategoryID = id.GetValueOrDefault(bl.Categories.Count == 0 ? 0 : bl.Categories.Min(c => c.ID)),
                Name = "Product name",
                Description = "Product description"
            }
        });
    }
} 

Provede se tedy jen zobrazení pohledu ProductDetails, který slouží k manipulaci s jedním produktem. Nastavíme, že pohled se má zobrazit v módu na přidávání nového produktu a pokud byla nastavena kategorie nově přidávaného produktu (tedy pokud parametr nebyl null), tak tuto kategorii deleguje do pohledu.

Seznam produktů

Pro předání dat z kontroleru do pohledu Index, který zobrazuje seznam produktů, použijeme model HomeIndexModel, který vypadá úplně stejně jako HomeCategoriesModel:

public class HomeIndexModel
{

    public List<Product> Products
    {
        get;
        set;
    }

    public List<Category> Categories
    {
        get;
        set;
    }

} 

I akce v kontroleru HomeController vypadá velice podobně – jen vytvoříme model a naplníme ho daty z business vrstvy:

[PageTitle("Products")]
public ActionResult Index()
{
    using (var bl = new BusinessLayer())
    {
        return View(new HomeIndexModel { Products = bl.Products, Categories = bl.Categories });
    }
} 

Trošku složitější ale tentokrát bude pohled:

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="SimpleMVCApplication.Views.Home.Index" %>

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
    
    <table class="table-centered" width="80%" border="1">
    
        <% bool logged = ViewData["user"] is LoggedUser; %>
    
        <tr><th>Product</th><th>Description</th><th>Category</th><% if (logged) { %><th>Actions</th><% } %></tr>
        
        <%  foreach (var product in ViewData.Model.Products)
            {
        %>
                <tr>
                    <%  if (logged)
                        {
                    %>
                            <td><%= Html.ActionLink<HomeController>(c => c.EditProduct(product.ID), product.Name) %></td>
                    <%  }
                        else
                        {
                    %>
                            <td><%= Html.ActionLink<HomeController>(c => c.ProductDetails(product.ID), product.Name) %></td>
                    <%
                        }
                    %>
                    
                    <td><%= Html.Encode(product.Description) %></td>
                    <td><%= Html.Encode(ViewData.Model.Categories.Find(c => c.ID == product.CategoryID).Name) %></td>
                    <%if (logged)
                      {
                    %>
                    <td align="center" valign="middle">
                        <%  using (Html.BeginForm<HomeController>(c => c.DeleteProduct(product.ID), FormMethod.Post))
                            {
                        %>
                                <%= Html.SubmitButton("delete", "Delete") %>
                        <%
                            }
                        %>
                    </td>
                    <% } %>
                </tr>
        <%
            }
        %>
    
    </table>
    
    <%  //using (Html.BeginForm("ProductOperation", "Home", FormMethod.Post))
        using(Html.BeginForm<HomeController>(c => c.ProductOperation(null, null), FormMethod.Post))
        {
    %>
            <table class="table-centered">
                <tr>
                    <th>Products</th>
                    <td>
                        <%= Html.DropDownList(null, "productID", new SelectList(ViewData.Model.Products, "ID", "Name"))%>
                    </td>
                    <td><%= Html.SubmitButton("operation", "DeleteProduct", logged ? null : new { disabled = "disabled"}) %></td>
                    <td><%= Html.SubmitButton("operation", "EditProduct", logged ? null : new { disabled = "disabled" })%></td>
                    <td><%= Html.SubmitButton("operation", "ProductDetails")%></td>
                </tr>
            </table>
    <%
        }
    %>
</asp:Content> 

Nejprve si do lokální proměnné logged uložíme informaci, jestli je uživatel zalogovaný a tedy jestli mu zobrazit tlačítka a odkazy na úpravu produktů.

V další části pak renderujeme tabulku, kde je pro každý produkt uveden název (který je zároveň odkazem na stránku s detaily produktu – pro zalogované uživatele s možností editace). Pro zalogované uživatele také zobrazíme tlačítko pro smazání produktu.

V další části pak zobrazíme produkty v combo-boxu. Ten zobrazíme pomocí metody Html.DropDownList, která bere jako první parametr první položku (tam by mohlo být něco jako „Vyberte produkt…“), druhý parametr je jméno combo-boxu (tedy v jaké proměnné dostaneme vybranou hodnotu v akci) a poslední parametr je instance typu SelectList. Jeho konstruktor bere tři parametry – kolekci zobrazovaných položek, jméno property každého prvku, která se použije jako hodnota, a nakonec jméno property, která se použije k zobrazení. Uvedeným zápisem docílíme toho, že se nám zobrazí v combo-boxu hodnoty Name produktů (uživatel vidí názvy produktů) a jako vybraná hodnota (tedy co nám přijde do akce) se použije hodnota property ID.
Dále zobrazíme tlačítka pro editaci, smazání a zobrazení detailu produktu. Všechna tlačítka pojmenujeme stejně, ale dáme jim jinou hodnotu. Tím docílíme toho, že nám do akce přijde parametr s názvem operation, jehož hodnota bude nastavena na hodnotu tlačítka – tím zjistíme, jaké tlačítko bylo stisknuto.

Akce zpracovávající stisk těchto tří tlačítek vypadá následovně:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult ProductOperation(int? productID, string operation)
{
    if (productID.HasValue)
    {
        return RedirectToAction(operation, new { id = productID.Value });
    }
    else
    {
        return RedirectToAction("Index", new { message = "Product ID not specified." });
    }
}

Provedeme tedy pouze přesměrování na akci, která je specifikována parametrem operation, ve kterém je uložena hodnota stisknutého tlačítka.

Za povšimnutí dále stojí, že pro vyrenderování formuláře používám klasicky generickou verzi metody Html.BeginForm, která mi umožní specifikovat odkazovanou akci silně typově. Zde jsme ale v situaci, kdy nevíme, jaké přesně předat akci parametry – ty totiž vybere až uživatel z combo-boxu. Zde je důležité si uvědomit, že první parametr metody Html.BeginForm není delegát, ale expression tree a tedy tento kód není niky přímo vykonán, ale je pouze zanalyzován pro vytvoření odkazu. A tato analýza je založena na tom, že pokud je hodnota nějakého parametru null, tak se předpokládá, že tato hodnota bude dosazena nějak jinak než námi zadanou hodnotou – v našem případě přes webový formulář. Proto je nutné mít první parametr akce ProductOperation nikoliv typu int, ale typu int?, abychom do něj mohli přiřadit null. Kdybychom ho totiž nechali typu int a zadali tam třeba nulu, tak při volání akce bychom vždy jako první parametr dostávali tu námi zadanou nulu.
Pokud budeme trvat na tom, že první parametr akce ProductOperation musí být typu int, tak musíme použít negenerickou verzi metody Html.BeginForm, jak je ukázáno v kódu v komentáři.

Tlačítko pro smazání produktu je ošetřeno touto akcí:

[PageTitle("Delete product"), SimpleMVCApplication.Aspects.Authorize]
public ActionResult DeleteProduct(int id)
{
    using (var bl = new BusinessLayer())
    {
        bool removed = bl.RemoveProduct(id);
        bl.SaveChanges();
        return RedirectToAction("Index", new { message = removed ? "Product deleted." : "Product not found." });
    }
} 

Jednoduše provedeme smazání zadaného produktu a pak se přesměrujeme na akci se seznamem produktů.

Detaily produktu

Pro zobrazení detailů produktu používáme pohled ProductDetails, který může pracovat ve třech režimech – prohlížení produktu, editace produktu nebo přidání nového produktu. Typ tohoto zobrazení je součástí modelu:

public class HomeProductDetailsModel
{

    public Product Product
    {
        get;
        set;
    }

    public List<Category> Categories
    {
        get;
        set;
    }
    
    public enum ViewType { Details, Add, Edit }

    public ViewType View
    {
        get;
        set;
    }

} 

Kromě typu zobrazení a produktu, nad kterým pracujeme, si předáváme také seznam všech kategorií.

Pohled ProductDetails používáme ze třech akcí: AddProduct (popsána v sekci o zobrazování seznamu kategorií), EditProduct a ProductDetails.

Akce EditProduct předává pohledu ProductDetails model takto:

[PageTitle("Edit product"), SimpleMVCApplication.Aspects.Authorize]
public ActionResult EditProduct(int id)
{
    using (var bl = new BusinessLayer())
    {
        var p = bl.GetProduct(id);
        if (p == null)
        {
            return RedirectToAction("Index", new { message = string.Format("Product with id {0} not found.", id) });
        }
        else
        {
            return View("ProductDetails", new HomeProductDetailsModel
            {
                View = HomeProductDetailsModel.ViewType.Edit,
                Categories = bl.Categories,
                Product = p
            });
        }
    }
} 

Pokud nenajdeme produkt s požadovaným ID, provedeme přesměrování na akci se seznamem produktů. V opačném případě zobrazíme pohled ProductDetails v editačním módu.

Velice podobně pracuje i akce ProductDetails – jediný rozdíl je v tom, že se pohled ProductDetails zobrazí v read-only módu.

public ActionResult ProductDetails(int id)
{
    using (var bl = new BusinessLayer())
    {
        var p = bl.GetProduct(id);
        if (p == null)
        {
            return RedirectToAction("Index", new { message = string.Format("Product with id {0} not found.", id) });
        }
        else
        {
            return View(new HomeProductDetailsModel
            {
                View = HomeProductDetailsModel.ViewType.Details,
                Categories = bl.Categories,
                Product = p
            });
        }
    }
} 

Vlastní pohled ProductDetails (reprezentovaný stránkou ProductDetails.aspx) není příliš složitý:


<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="ProductDetails.aspx.cs" Inherits="SimpleMVCApplication.Views.Home.ProductDetails" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">

    <%  using (Html.BeginForm(ViewData.Model.View.ToString(), "Home", FormMethod.Post))
        {
    %>
        <table class="table-centered" width="80%" border="1">
            <% var htmlAttributes = ViewData.Model.View != HomeProductDetailsModel.ViewType.Details ? null : new { disabled = "disabled" }; %>
            <tr>
                <td>Name: </td>
                <td><%= Html.TextBox("model.Product.Name", null, htmlAttributes)%></td>
            </tr>
            <tr>
                <td>Description: </td>
                <td><%= Html.TextBox("model.Product.Description", null, htmlAttributes)%></td></tr>
            <tr>
                <td>Category: </td>
                <td><%= Html.DropDownList(null, "model.Product.CategoryID", new SelectList(ViewData.Model.Categories, "ID", "Name", ViewData.Model.Product.CategoryID), htmlAttributes)%></td>
            </tr>
            <%  if (ViewData.Model.View != HomeProductDetailsModel.ViewType.Details)
                {
            %>
                    <tr>
                        <td><%= Html.Hidden("model.Product.ID")%> </td>
                        <td><%= Html.SubmitButton("submit", ViewData.Model.View.ToString())%></td>
                    </tr>
            <%
                }
            %>    
        </table>
    <%
        }
    %>

</asp:Content>

Akce, která zpracuje formulář, je specifikována vtipně pomocí položky ViewData.Model.View, která je typu enum a udává způsob zobrazení formuláře (Detail, Edit, Add). Zbytek vychází z pohledů, o kterých jsme již mluvili. Jediná nová věc je použití metody Html.Hidden, která slouží k vyrenderování hidden formulářového pole, pomocí které si poznamenáme ID produktu, který editujeme.

Už nám tedy stačí ukázat, jak vypadá akce pro přijmutí editovaných údajů a pro přijmutí parametrů nového produktu:

[SimpleMVCApplication.Aspects.Authorize]
public ActionResult Add(HomeProductDetailsModel model)
{
    using (var bl = new BusinessLayer())
    {
        if (string.IsNullOrEmpty(model.Product.Name))
        {
            ViewData.ModelState.AddModelError("name", string.Empty, "Name cannot be empty string.");
            model.View = HomeProductDetailsModel.ViewType.Add;
            model.Categories = bl.Categories;
            return View("ProductDetails", model);
        }
        var p = bl.AddProduct(model.Product.Name, model.Product.Description, model.Product.CategoryID);
        bl.SaveChanges();
        return RedirectToAction("EditProduct", new { id = p.ID, message = "Product successfully added." });
    }
}

[SimpleMVCApplication.Aspects.Authorize]
public ActionResult Edit(HomeProductDetailsModel model)
{
    using (var bl = new BusinessLayer())
    {
        if (string.IsNullOrEmpty(model.Product.Name))
        {
            ViewData.ModelState.AddModelError("name", string.Empty, "Name cannot be empty string.");
            model.View = HomeProductDetailsModel.ViewType.Edit;
            model.Categories = bl.Categories;
            return View("ProductDetails", model);
        }
        if (bl.UpdateProduct(model.Product.ID, model.Product.Name, model.Product.Description, model.Product.CategoryID))
        {
            bl.SaveChanges();
            return RedirectToAction("EditProduct", new { id = model.Product.ID, message = "Product successfully updated." });
        }
        else
        {
            return RedirectToAction("Index", new { message = string.Format("Product with id {0} not found.", model.Product.ID) });
        }
    }
} 

V obou případech nejprve zkontrolujeme, zda je zadáno jméno produktu, a pokud ne, tak uložíme chybovou hlášku do ViewData.ModelState a formulář znovu zobrazíme. Toto by šlo samozřejmě vyřešit systémověji (např. provádět kontrolu až v business vrstvě a tam vyhazovat výjimku).

V opačném případě, tedy při zadání správných údajů, provedeme příslušnou operaci nad business vrstvou a provedeme přesměrování na vhodnou akci – tou je v našem případě zobrazení pohledu s editovací produktu. Mohli bychom zde provést místo přesměrování rovnou zobrazení daného pohledu, ale budeme se raději řídit Post/Redirect/Get patternem, který nám zajistí, že nemůže dojít k vícenásobnému odeslání téhož formuláře, což by sice u editace produktu tolik vadit nemuselo, ale u přidání produktu ano.

Shrnutí

Ukázali jsme si, jak na typické postupy z vývoje webových aplikací při použití frameworku ASP.NET MVC, konkrétně ve verzi ASP.NET MVC Beta. Nejlepší podle mě je napsat si takovouto aplikaci sám a základní principy si pěkně osahat a vyzkoušet. V takovém případě doufám, že můj článek poslouží jako dobrá inspirace. Jestli někdo dočetl až jsem (to bych docela koukal 🙂 ) a něco není jasné (to bych se vůbec nedivil 🙂 ), tak ať neváhá napsat dotaz do komentářů a já rád vysvětlím, příp. doplním článek.

Zanechat odpověď

Vyplňte detaily níže nebo klikněte na ikonu pro přihlášení:

Logo WordPress.com

Komentujete pomocí vašeho WordPress.com účtu. Odhlásit /  Změnit )

Facebook photo

Komentujete pomocí vašeho Facebook účtu. Odhlásit /  Změnit )

Připojování k %s

Tento web používá Akismet na redukci spamu. Zjistěte více o tom, jak jsou data z komentářů zpracovávána.