Category Archives: ASP .NET

ASP .NET MVC, formularze

Formularz w ASP .NET MVC można zdefiniować za pomocą czystego HTML np:

<form action="Clients/Create" method="post">
    <input type="text" />
    <input type="submit" />
</form>

Znacznie wygodniejszym rozwiązaniem jest jednak użycie metod dostarczonych przez helpery. W ASP .NET MVC podstawowym helperem jest HTMLHelper do którego można uzyskać referencję w widoku za pomocą właściwości HTML. Stworzenie formularza wygląda więc następująco:

<% using (Html.BeginForm())
    { %>
    
<%}%>

Domyślnie zostanie użyta akcja w której się znajdujemy – np. /Clients/Create. Można również zdefiniować konkretny kontroler, akcję i tzw. route values:

<% using (Html.BeginForm("akcja","kontroler",new{id=3},FormMethod.Get,null))

    { %>
<%}%>

Powyższy formularz typu get wywoła w zależności od ustawień routingu “/kontroler/akcja/3” lub “/kontroler/akcja/?id=3 “.

W analogiczny sposób za pomocą helpera można definiować poszczególne pola:

<%=Html.TextBox("FirstName",Model.Client.FirstName) %>
<%=Html.TextAreaFor(Model.Product.Description) %>

Ponadto pola mogą być weryfikowane pod kątem zawartości. Najpierw zobaczmy jak w widoku można oznaczać takie pola:

 <% using (Html.BeginForm())
       { %>
       <%=Html.ValidationSummary() %>

            <%=Html.TextBox("FirstName",Model.Client.FirstName) %>
            <%=Html.ValidationMessage("FirstName", "*")%> 
<%}>

ValidationSummary pokazuje szczegółowy opis walidacyjny z wszystkich pól. Z kolei ValidationMessage odnosi się wyłącznie do pojedynczego pola. W tym przypadku gdy imię nie przejdzie walidacji pozytywnie zostanie wyświetlona gwiazdka. W kontrolerze można walidować dane i wszelkie błędy zgłaszać za pomocą AddModelError:

public ActionResult Create()
{
 ...
 this.ModelState.AddModelError("FirstName", "Nieprawdiłowe imię");
 ...
}

W następnym poście napiszę o MasterPages widokach częściowych – zapraszam.

ASP .NET MVC – definiowanie widoku, podstawy

Widok czyli graficzny interfejs użytkownika definiuje się w ASP .NET MVC za pomocą kilku helperów oraz czystego HTML\XHTML. Prosty widok prawie niczym nie różni się od dokumentu XHTML:

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage" %>

<!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>ViewPage1</title>
</head>
<body>
    <div>
    
    </div>
</body>
</html>

Jedyną nowością jest dodanie deklaracji Page oraz atrybutu runat=”server”.

Jak jednak pamiętamy z poprzedniego postu, kontroler do widoku przekazuje tzw. model:

public ActionResult Index()
{
    Model model=new Model();
    model.FirstName="Piotr";
    model.LastName="Nazwisko"
    return View(model);
}

Model to dowolna klasa zdefiniowana przez użytkownika np:

public class Model
{
   public string FirstName { get; set; }
   public string LastName { get; set; }
}

W jaki sposób dostać się  do modelu w widoku? Przede wszystkim należy stworzyć tzw.strong type view (widok z silną typizacją). Najłatwiej  zrealizować to za pomocą kreatora (Add->View):

image

W kreatorze zaznaczamy opcję “Create a strongly-typed view” oraz wybieramy klasę Model. Dzięki temu zostanie wygenerowany następujący kod:

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

<!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>SampleView</title>
</head>
<body>
    <div>
    
    </div>
</body>
</html>

Jak widać, kod w atrybucie Inherits ma przypisany typ generyczny System.Web.Mvc.ViewPage<MvcApplication1.Models.Model>. W tej chwili można już za pomocą odpowiednich dyrektyw dostać się do zawartości modelu:

<div>
Your first name is: <%=Model.FirstName %>
Your last name is: <%=Model.LastName %>
</div>

Dyrektywa <%=%> wyświetla po prostu zawartość zmiennej. W identyczny sposób można zwracać wartości funkcji.

Inną dyrektywą jest <% jakiś kod %>. Wykonuje ona dowolny kod:

<div>
    <%string firstName=Model.FirstName;
    if (firstName.StartsWith("P"))
    {          
        %>
    Your first name is: <%=Model.FirstName%>
    Your last name is: <%=Model.LastName%>
    <%} %>
</div>

W najnowszej wersji frameworka dodano jeszcze jedną dyrektywę: <%: %>. Służy ona do automatycznego kodowania tekstu (ochrona przed XSS). Przykładowo <%:Model.FirstName%> jest  równoważne wywołaniu <%=Html.Encode(Model.FirstName)%>.

Oprócz wspomnianych strong-typed view, można definiować widoki bez podania typu modelu. W takim przypadku deklaracja Page wygląda następująco (brak typu generycznego ViewPage):

<%@ Page Language="C#"  Inherits="System.Web.Mvc.ViewPage" %>

Z kolei wyświetlanie zawartości:

<%= Html.Encode(ViewData["FirstName"]) %>

W następnym poście pokaże jak definiować własne formularze i walidować dane.

Własna baza danych oraz ASP .NET Membership

Domyślnie dane o użytkownikach przetrzymywane się w osobnej bazie danych aspnetdb (SQL Server Express). Czasami jednak warto aby wpisy o użytkownikach były zapisywane w naszej bazie danych. Wyobraźmy sobie portal społecznościowy, w którym użytkownicy mogą umieszczać zdjęcia. W takim przypadku w bazie danych z pewnością będzie tabela łącząca wysłane fotografie z użytkownikami. Jeśli użytkownicy są w jednej bazie danych mamy łatwą kontrolę nad relacjami – wystarczy użyć klucza obcego wskazującego na klucz główny użytkownika. Ponadto często hosting ASP .NET nie wspiera lokalnych baz aspnetdb i jedynym sposobem jest umieszczenie danych membership w głównej bazie:

  1. Pierwszym krokiem jest wygenerowanie odpowiednich tabel oraz procedur (tych, które znajdują się w aspnetdb). Służy do tego narzędzie Aspnet_regsql.exe, które możecie znaleźć w katalogu c :\%windir%\Microsoft.NET\Framework\<versionNumber>. Po odpaleniu programu należy wskazać docelową bazę danych, typ uwierzytelnia itp. Kreator większość zrobi za nas – na koniec tego etapu struktura bazy danych powinna zostać wygenerowana.

    image

  2. W celu upewnienia się, można otworzyć bazę danych i obejrzeć jej strukturę. Widać, że zostały wygenerowane tabele:

    image

    1. Kolejnym krokiem jest dodanie odpowiedniego connectionString’a w pliku web.config:

      <connectionStrings>
          <add name="MembershipTestDB" connectionString="Data Source=PIOTR-PC;Initial Catalog=MembershipTest;Integrated Security=True;MultipleActiveResultSets=True" providerName="System.Data.SqlClient" />
      </connectionStrings>
      

    2. Mamy już skonfigurowaną bazę danych. Pozostało już tylko skonfigurować aplikację tak aby korzystała z tej bazy danych:
      <membership>
              <providers>
                  <clear/>
                  <add name="AspNetSqlMembershipProvider" 
                      connectionStringName="MembershipTestDB" 
                      applicationName="/"
      type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
              </providers>
          </membership>
      
      <roleManager enabled="true">
        <providers>
          <clear/>
          <add name="AspNetSqlRoleProvider"
              connectionStringName="MembershipTestDB"
              applicationName="/"
              type="System.Web.Security.SqlRoleProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/>
        </providers>
      </roleManager>
      
      

    ASP .NET Membership w WinForms.

    ASP .NET Membership jest bardzo dobrym rozwiązaniem dla uwierzytelnienia oraz autoryzacji użytkowników w aplikacji webowej. Umożliwia m.in. zarządzanie kontami użytkowników czy tworzenie ról. Często jednak system składa się również z aplikacji typu desktop. W rozbudowanych systemach proponuję zrezygnować z czystego ASP .NET Membership na rzecz Windows Identity Framework. Jeśli jednak tworzymy małą aplikacje z pewnością przyda nam się wsparcie ASP .NET Membership w WinForms.

    Rozwiązaniem problemu są tzw. client application services. Zacznijmy jednak od samego początku:

    1. Najpierw tworzymy oczywiście aplikację web ASP .NET.
    2. Oprócz autoryzacji istnieje również możliwość udostępniania właściwości aplikacji WinForms. Dodajemy przykładową właściwość:
      <profile>
        <properties>
            <add name="Color" type="string" defaultValue="Blue"/>
          </properties>
      </profile>
      ...
      </system.web>

    3. W celu udostępnienia właściwości Color wystarczy w w web.config dodać następujący wpis:
      <system.web.extensions>
          <scripting>
              <webServices>
                  <authenticationService enabled="true" />
                  <roleService enabled="true"/>
                  <profileService enabled="true" readAccessProperties="Color" writeAccessProperties="Color"/>
              </webServices>        
          </scripting>
      </system.web.extensions>

    Powyższy kod również udostępnia usługę uwierzytelnienia oraz zarządzania rolami.

    Następnym etapem jest konfiguracja aplikacji WinForms:

    1. Tworzymy aplikacje typu WindowsForms.
    2. Wchodzimy we właściwości projektu a konkretnie w zakładkę Services. Następnie ustawiamy prawidłowy adres. Jeśli zakłada nie jest aktywna to prawdopodobnie  został ustawiony .NET Framework Client Profile i należy ustawić np. NET Framework 4.0.

      image 

    3. Wchodzimy w zakładkę Settings oraz klikamy w przycisk Load Web Settings. Po wpisaniu loginu i hasła zostaną zwrócone udostępnione właściwości:

    image 

    Od tej pory w kodzie aplikacji WinForms możemy odwoływać się do właściwości jak do zwykłych zasobów, np:

    public Form1()
    {
      InitializeComponent();
      if(Login())
        this.BackColor = Color.FromName(Properties.Settings.Default.Color) ;
    }
    private bool Login()
    {
      bool result = System.Web.Security.Membership.ValidateUser("admin", "password");
      if (result)
          MessageBox.Show("Zalogowano.", "Autoryzacja", MessageBoxButtons.OK,MessageBoxIcon.Information);
      else
          MessageBox.Show("Nieprawidłowy login lub hasło.", "Autoryzacja", MessageBoxButtons.OK,MessageBoxIcon.Error);
    
      return result;
    }

    Podobnie można zapisywać właściwość:

    Properties.Settings.Default.Color = Color.Red.ToString();
    Properties.Settings.Default.Save();

    W celu przekonania się, że to naprawdę działa ustawmy kolor również w aplikacji web:

    protected void Page_Load(object sender, EventArgs e)
    {                                        
      Button1.BackColor = System.Drawing.Color.FromName(Context.Profile.GetPropertyValue("Color").ToString());
    }

    Oczywiście aby kod zadziałał użytkownik musi być zalogowany (np. za pomocą gotowej kontrolki Login).

    W podobny sposób można używać metody InRole (sprawdzanie czy użytkownik należy do danej grupy). Warto podkreślić, że za pomocą client application service można korzystać z Membership również w aplikacjach WPF, AJAX czy nawet Silverlight.

    Asynchroniczne strony ASP .NET

    W celu wyjaśnienia zasady działania asynchronicznych stron, najpierw przyjrzyjmy się jak wygląda standardowe zapytanie do serwera. Klient wysyła żądanie HTTP do serwera np. typu GET w celu uzyskania danej strony www. Następnie serwer używa tzw. puli wątków (thread pool). Po prostu przydziela wątek z puli każdemu nadchodzącemu żądaniu. Tworzenie (a raczej odtwarzanie) wątków z puli jest szybkie (o tym już pisałem kiedyś), jednak liczba wątków jest ograniczona. W przypadku gdy serwer będzie musiał obsłużyć zbyt dużą liczbę żądań, zakończy się to błędem “503 – server unavaiable”. Obsługa większości zapytań nie stanowi problemu, jednak gdy aplikacja ASP .NET korzysta z usługi sieciowej lub przetwarza ogromną ilość danych może to stanowić poważne zredukowanie skalowalności.Wyobraźmy sobie, że nasza aplikacja www korzysta z usługi sieciowej. Ponadto połączenie z usługa jest dość wolne – trwa 30 sekund. Gdy przyjdzie zapytanie do serwera, zostanie zdjęty wątek z puli i będzie on zarezerwowany przez ponad 30 sekund (czas połączenia z usługą + inne operacje). Przez ten cały czas wątek nie będzie mógł być wykorzystany do obsługi innych żądań. Co gorsza, wątek większość czasu straci po prostu na oczekiwanie odpowiedzi od usługi sieciowej. Należy podkreślić, że wątki ASP .NET są zbyt cenne aby marnować je na operacje typu IO. Jeśli w systemie rzeczywiście musimy połączyć się z zewnętrznymi zasobami, zróbmy to asynchronicznie!

    W przypadku asynchronicznych stron po odebraniu żądania również jest zdejmowany wątek z puli. Jednak zaraz po stwierdzeniu, że mamy do czynienia z asynchroniczną stroną www, wątek jest z powrotem oddawany puli. W tym momencie rozpoczyna się wykonywanie asynchronicznego kodu – czyli np. ładowania danych z usługi sieciowej czy z bazy danych. Po zakończeniu, wątek z powrotem jest zdejmowany z puli i rozpoczyna się już normalne przetwarzanie strony www. Innymi słowy, używamy asynchronicznego przetwarzania dla bardzo czasochłonnych operacji (przeważnie są to operacje typu IO). Na poniższym rysunku przedstawiam standardowy przebieg obsługi żądania w stronach synchronicznych:

     

     

    image

     

     

    Nie ma tu nic nadzwyczajnego. W przypadku asynchronicznych stron, wątek z puli będzie zwolniony w pewnym momencie i sterowanie zostanie przekazane odpowiednim asynchronicznym metodom:

     

     

    image

    Przejdźmy teraz do implementacji tego w ASP .NET. W wersji 2.0 zostało to znacząco uproszczone. Przede wszystkim należy ustawić atrybut Async na true w dyrektywie @Page:

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

    Ustawienie atrybutu na true tak naprawdę oznacza, że zostanie wykorzystany  IHttpAsyncHandler.

    Aby rozpocząć część asynchroniczną strony należy wywołać metodę AddOnPreRenderCompleteAsync przekazując odpowiednie handlery:

    protected void Page_Load(object sender, EventArgs e)
    {
      AddOnPreRenderCompleteAsync(new BeginEventHandler(BeginAsyncOperation), new EndEventHandler(EndAsyncOperation));
    }
    private IAsyncResult BeginAsyncOperation(object sender, EventArgs e,
    AsyncCallback cb, object state)
    {
      m_Request = WebRequest.Create("http://www.pzielinski.com");
      return m_Request.BeginGetResponse(cb, state);
    }
    private void EndAsyncOperation(IAsyncResult ar)
    {
      string text;
      using (WebResponse response = this.m_Request.EndGetResponse(ar))
      {
          using (StreamReader reader =
              new StreamReader(response.GetResponseStream()))
          {
              text = reader.ReadToEnd();
          }
      }
       Response.Write(text);
    }

    W podobny sposób można pobrać dane z bazy danych i przekazać je odpowiedniej kontrolce renderującej (np. ListView).

    Ponadto w ASP .NET można definiować tzw. zadania asynchroniczne. Sprawa wygląda podobnie jak w przypadku powyższego kodu. Zadania asynchroniczne wprowadzają dodatkowo kilka rozszerzeń:

    1. Oprócz metod BEGIN i END można zdefiniować metodę odpowiedzialną za wykonanie kodu w razie timeout. Wartość timeout można zdefiniować również w dyrektywie @Page za pomocą atrybutu AsyncTimeout.
    2. Można rejestrować kilka zadań  – dopiero po zakończeniu wszystkich, zostanie wznowione przetwarzanie synchroniczne.
    3. Rejestrując zadanie można dodatkowo przekazać jakiś argument jako object (wykorzystywany w metodach BEGIN) .

    Przykład:

    protected void Page_Load(object sender, EventArgs e)
    {
      RegisterAsyncTask(new PageAsyncTask(BeginAsyncOperation, EndAsyncOperation, TimeoutOperation, "state"));
    }
    private IAsyncResult BeginAsyncOperation(object sender, EventArgs e,
    AsyncCallback cb, object state)
    {
      m_Request = WebRequest.Create("http://www.pzielinski.com");
      return m_Request.BeginGetResponse(cb, state);
    }
    private void EndAsyncOperation(IAsyncResult ar)
    {
      string text;
      using (WebResponse response = this.m_Request.EndGetResponse(ar))
      {
          using (StreamReader reader =
              new StreamReader(response.GetResponseStream()))
          {
              text = reader.ReadToEnd();
          }
      }
      Response.Write(text);
    }
    private void TimeoutOperation(IAsyncResult ar)
    {
    
    }

    Filtrowanie QueryString za pomocą maksymalnej długości

    IIS 7.0 ze względu bezpieczeństwa ogranicza długość tzw. QueryString do 2048 bajtów. Czasami okazuje się, że długość jest zbyt ograniczająca i nie da się wykonać pewnych operacji takich jak np. przekazanie tokena, który zwykle zajmuje więcej niż naniesione ograniczenie. Na szczęście w pliku konfiguracyjnym web.config można łatwo ustawić dowolną maksymalną długość QueryString:

    <system.webServer>
        <security>
            <requestFiltering>        
                <requestLimits maxQueryString="5000" />        
            </requestFiltering>    
        </security>
    </system.webServer>

    Dobrą zasadą jest również ustawianie długości jak najmniejszej, tak aby wystarczała tylko na przewidziane przypadki użycia.