niedziela, 15 maja 2011

Własna implementacja MembershipProvider (Custom MembershipProvider)

We wcześniejszym poście wspominałem w jaki sposób korzystać z MembershipProvidera. Provider ten oferuje możliwość korzystania z autentyfikacji przy użyciu kontrolek ASP.NET. O ile dobrze jest umieć wykorzystać ten mechanizm w formie takiej jakiej jest często przydaje się możliwość ingerowania w niego i stworzenia własnego Providera.

Jakie możliwości daje nam własny provider? Otóż daje przede wszystkim wszechstronność i elastyczność w użyciu:
- nadaje się świetnie tam gdzie, mamy schemat już gotowy i chcemy wykorzystać istniejącą strukturę tabel
- nie podoba się nam koncepcja Microsoftu w kwestii pól w tabeli użytkowników
- chcemy zmienić sposób działania mechanizmu 
- mamy zamiar stworzyć Providera dla innego systemu bazodanowego niż MS SQL (choć do wielu popularnych silników bazodanowych są już stworzone providery)

Na początek przyjrzyjmy się przykładowej strukturze tabeli:


Taki schemat może istnieć w np. już istniejącym projekcie do którego chcemy dołożyć możliwość korzystania z zalet MembershipProvidera.

Na początek stworzymy klasę MyUser dziedziczącą po klasie MembershipUser. Klasa ta będzie implementować dodatkowe pola, który znajdują się w tabeli naszej bazy danych. Implementacja jest bardzo prosta:


Code:
public class MyUser : MembershipUser
    {
        private string firstName;

        public string FirstName
        {
            get { return firstName; }
            set { firstName = value; }
        }

        private string lastName;

        public string LastName
        {
            get { return lastName; }
            set { lastName = value; }
        }

        private DateTime birthDate;

        public DateTime BirthDate
        {
            get { return birthDate; }
            set { birthDate = value; }
        }

        private int idUser;

        public int IdUser
        {
            get { return idUser; }
            set { idUser = value; }
        }


        public MyUser(string providerName, string name, object providerUserKey, 
            string email, string passwordQuestion, string comment, bool isApproved, 
            bool isLockedOut, DateTime creationDate, DateTime lastLoginDate, 
            DateTime lastActivityDate, DateTime lastPasswordChangedDate, 
            DateTime lastLockoutDate, int idUser, string firstName, string lastName,
            DateTime birthDate)
            : base(providerName, name, providerUserKey, email, passwordQuestion, 
            comment, isApproved, isLockedOut, creationDate, lastLoginDate, 
            lastActivityDate, lastPasswordChangedDate, lastLockoutDate)
        {
            FirstName = firstName;
            LastName = lastName;
            BirthDate = birthDate;
        }
    }

Mając nową implementację klasy MembershipUser możemy jej użyć podczas implementacji MembershipProvider-a.

Aby zaimplementować własnego MembershipProvidera który używa innej implementacji MembershipUsera niż standardowa należy w klasie dziedziczącej po MembershipProvider wykonać następujące trzy czynności:
1. W metodach (pierwsza przyjmuje jako argumenty string i bool, druga object i bool) GetUser zwracamy instancję naszej klasy MembershipUser (oczywiście metoda nadal musi zwracać jako typ MembershipUser - ale oczywiście każdy zna zasady dziedziczenia i raczej rozumie o co chodzi w tym)
2. UpdateUser - w tej metodzie przyjmujemy obiekt typu MembershipUser. Obiekt ten musimy rzutować na nową implementację naszego MembershipUser.
3. CreateUser - metoda ta to jedna z najważniejszych metod. Pozwala na utworzenie nowego użytkownika w naszej aplikacji. Nasza klasa dziedzicząca po MembershipProvider musi posiadać podstawową wersję metody CreateUser. Aby utworzyć użytkownika z nowymi parametrami musimy stworzyć przeładowanie tej metody zawierające dodatkowe pola. Oprócz tego jako wynik działania klasy zwracamy nasz nowy typ użytkownika.

Tak więc moja implementacja powyższych metod przedstawia się następująco:


Code:
public class MyProvider : MembershipProvider
    {
        private string name = "app";
        private string connectionString = ConfigurationManager.ConnectionStrings[1].ConnectionString;

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

        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            throw new NotImplementedException();
        }

        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            throw new NotImplementedException();
        }

        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
        {
            ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username, password, true);
            OnValidatingPassword(args);
            if (args.Cancel)
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }

            if (RequiresUniqueEmail && GetUserNameByEmail(email) != "")
            {
                status = MembershipCreateStatus.DuplicateEmail;
                return null;
            }
            MembershipUser user = GetUser(username, false);
            if (user == null)
            {
                DateTime createDate = DateTime.Now;
                using (SqlConnection conn = new SqlConnection(connectionString))
                {
                    using (SqlCommand cmd = new SqlCommand())
                    {
                        byte[] clearPassword = Encoding.ASCII.GetBytes(password);
                        byte[] encryptetPassword = EncryptPassword(clearPassword);
                        cmd.CommandText = @"INSERT INTO Users(UserName, UserNameLowerCase, Password, PasswordSalt, FirstName, LastName, BirthDate, Email)
                                            VALUES(@UserName, @UserNameLowerCase, @Password, @PasswordSalt, @FirstName, @LastName, @BirthDate, @Email)";
                        cmd.Parameters.AddWithValue("@UserName", username);
                        cmd.Parameters.AddWithValue("@UserNameLowerCase", username.ToLower());
                        cmd.Parameters.AddWithValue("@Password", Convert.ToBase64String(encryptetPassword));
                        cmd.Parameters.AddWithValue("@PasswordSalt", password);
                        cmd.Parameters.AddWithValue("@Email", email);
                        try
                        {
                            cmd.Connection = conn;
                            conn.Open();
                            int iRecAdded = cmd.ExecuteNonQuery();
                            if (iRecAdded > 0)
                            {
                                status = MembershipCreateStatus.Success;
                            }
                            else
                            {
                                status = MembershipCreateStatus.UserRejected;
                            }
                        }
                        catch (Exception ex)
                        {
                            status = MembershipCreateStatus.UserRejected;
                        }
                        finally
                        {
                            conn.Close();
                        }
                        return GetUser(username, false);
                    }
                }

            }
            else
            {
                status = MembershipCreateStatus.DuplicateUserName;
            }
            return null;
        }

        public MyUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status, string firstName, string lastName, DateTime birthDate)
        {
            ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username, password, true);
            OnValidatingPassword(args);
            if (args.Cancel)
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }

            if (RequiresUniqueEmail && GetUserNameByEmail(email) != "")
            {
                status = MembershipCreateStatus.DuplicateEmail;
                return null;
            }
            MembershipUser user = GetUser(username, false);
            if (user == null)
            {
                DateTime createDate = DateTime.Now;
                using (SqlConnection conn = new SqlConnection(connectionString))
                {
                    using (SqlCommand cmd = new SqlCommand())
                    {
                        byte[] clearPassword = Encoding.ASCII.GetBytes(password);
                        byte[] encryptetPassword = EncryptPassword(clearPassword);
                        cmd.CommandText = @"INSERT INTO Users(UserName, UserNameLowerCase, Password, PasswordSalt, FirstName, LastName, BirthDate, Email)
                                            VALUES(@UserName, @UserNameLowerCase, @Password, @PasswordSalt, @FirstName, @LastName, @BirthDate, @Email)";
                        cmd.Parameters.AddWithValue("@UserName", username);
                        cmd.Parameters.AddWithValue("@UserNameLowerCase", username.ToLower());
                        cmd.Parameters.AddWithValue("@Password", Convert.ToBase64String(encryptetPassword));
                        cmd.Parameters.AddWithValue("@PasswordSalt", password);
                        cmd.Parameters.AddWithValue("@FirstName", firstName);
                        cmd.Parameters.AddWithValue("@BirthDate", birthDate);
                        cmd.Parameters.AddWithValue("@Email", email);
                        try
                        {
                            cmd.Connection = conn;
                            conn.Open();
                            int iRecAdded = cmd.ExecuteNonQuery();
                            if (iRecAdded > 0)
                            {
                                status = MembershipCreateStatus.Success;
                            }
                            else
                            {
                                status = MembershipCreateStatus.UserRejected;
                            }
                        }
                        catch (Exception ex)
                        {
                            status = MembershipCreateStatus.UserRejected;
                        }
                        finally
                        {
                            conn.Close();
                        }
                        return (MyUser)GetUser(username, false);
                    }
                }
            }
            else
            {
                status = MembershipCreateStatus.DuplicateUserName;
            }
            return null;
        }

        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            throw new NotImplementedException();
        }

        public override bool EnablePasswordReset
        {
            get { throw new NotImplementedException(); }
        }

        public override bool EnablePasswordRetrieval
        {
            get { throw new NotImplementedException(); }
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        public override int GetNumberOfUsersOnline()
        {
            throw new NotImplementedException();
        }

        public override string GetPassword(string username, string answer)
        {
            throw new NotImplementedException();
        }

        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            MyUser user = null;
            SqlDataReader reader = null;
            using (SqlConnection conn = new SqlConnection(connectionString))
            {
                using (SqlCommand cmd = new SqlCommand())
                {
                    cmd.CommandText = @"SELECT IdUser, UserName, UserNameLowerCase, Password, PasswordSalt, FirstName, LastName, BirthDate
                                        FROM Users 
                                        WHERE UserNameLowerCase == @UserNameLowerCase";
                    cmd.Connection = conn;
                    cmd.Parameters.AddWithValue("@UserNameLowerCase", username);
                    try
                    {
                        conn.Open();
                        reader = cmd.ExecuteReader();
                        if (reader.HasRows)
                        {
                            reader.Read();
                            user = GetUserFromReader(reader);
                        }

                    }
                    catch (Exception e)
                    {
                    }
                    finally
                    {
                        if (reader != null)
                        {
                            reader.Close();
                        }
                        conn.Close();
                    }
                    return user;
                }
            }
        }

        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            throw new NotImplementedException();
        }

        public override string GetUserNameByEmail(string email)
        {
            throw new NotImplementedException();
        }

        public override int MaxInvalidPasswordAttempts
        {
            get { throw new NotImplementedException(); }
        }

        public override int MinRequiredNonAlphanumericCharacters
        {
            get { throw new NotImplementedException(); }
        }

        public override int MinRequiredPasswordLength
        {
            get { throw new NotImplementedException(); }
        }

        public override int PasswordAttemptWindow
        {
            get { throw new NotImplementedException(); }
        }

        public override MembershipPasswordFormat PasswordFormat
        {
            get { throw new NotImplementedException(); }
        }

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

        public override bool RequiresQuestionAndAnswer
        {
            get { throw new NotImplementedException(); }
        }

        public override bool RequiresUniqueEmail
        {
            get { throw new NotImplementedException(); }
        }

        public override string ResetPassword(string username, string answer)
        {
            throw new NotImplementedException();
        }

        public override bool UnlockUser(string userName)
        {
            throw new NotImplementedException();
        }

        public override void UpdateUser(MembershipUser user)
        {
            using (SqlConnection conn = new SqlConnection(connectionString))
            {
                using (SqlCommand cmd = new SqlCommand())
                {
                    MyUser myUser = user as MyUser;
                    if (myUser != null)
                    {
                        cmd.Connection = conn;
                        cmd.CommandText = @"UPDATE Users SET FirstName = @FirstName, LastName = @LastName, BirthDate = @BirthDate
                                        WHERE UserNameLowerCase = @UserNameLowerCase";
                        cmd.Parameters.AddWithValue("@FirstName", myUser.FirstName);
                        cmd.Parameters.AddWithValue("@LastName", myUser.LastName);
                        cmd.Parameters.AddWithValue("@BirthDate", myUser.BirthDate);
                        cmd.Parameters.AddWithValue("@UserNameLowerCase", myUser.UserName.ToLower());
                        try
                        {
                            cmd.ExecuteNonQuery();
                        }
                        catch (Exception ex)
                        {
                        }
                        finally
                        {
                            conn.Close();
                        }
                    }
                }
            }
        }

        public override bool ValidateUser(string username, string password)
        {
            throw new NotImplementedException();
        }

        private MyUser GetUserFromReader(SqlDataReader reader)
        {
            string username = reader["UserName"].ToString();
            string email = reader["Email"].ToString();
            int idUser = int.Parse(reader["IdUser"].ToString());
            string firstName = reader["FirstName"].ToString();
            string lastName = reader["LastName"].ToString();
            DateTime birthDate = DateTime.MinValue;
            if (reader["BirthDate"] != DBNull.Value)
            {
                birthDate = (DateTime)reader["BirthDate"];
            }
            MyUser u = new MyUser("", username, null, email, "", "", false, false, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue,
                DateTime.MinValue, DateTime.MinValue, idUser, firstName, lastName, birthDate);
            return u;
        }
    }


Część metod jak widać nie jest zaimplementowana - nie wszystko jest wymagane. Jeżeli któraś jest nam potrzebna - implementujemy ją. W przeciwnym razie nie ma potrzeby implementowania czegoś, co nie będzie używane.

Kolejnym etapem jest dodanie Providera do Web.configa:


Code:
<?xml version="1.0"?>

<!--
  For more information on how to configure your ASP.NET application, please visit
  http://go.microsoft.com/fwlink/?LinkId=169433
  -->

<configuration>
  <connectionStrings>
    <clear/>
    <add name="Db" providerName="System.Data.SqlClient" connectionString="Data Source=localhost;Initial Catalog=test2;Integrated Security=True"/>
  </connectionStrings>
    <system.web>
        <compilation debug="true" targetFramework="4.0" />
        <membership defaultProvider="MyProvider" userIsOnlineTimeWindow="15">
          <providers>
            <clear/>
            <add name="MyProvider" 
              type="WebApplication7.MyProvider"
              enablePasswordRetrieval="false"
              enablePasswordReset="false"
              requiresQuestionAndAnswer="false"
              applicationName="/"
              requiresUniqueEmail="false"
              passwordFormat="Clear"
              description="Stores and retrieves membership data from SQL Server"
              decryptionKey="68d288624f967bce6d93957b5341f931f73d25fef798ba75"
              validationKey="65a31e547b659a6e35fdc029de3acce43f8914cb1b2
                         4fff3e1aef13be438505b3f5becb5702d15bc7b98cd
                         6fd2b7702b46ff63fdc9ea8979f6508c82638b129a"
          />
          </providers>
        </membership>
      <machineKey
validationKey= 
"ADD36F43434EFE7750E6E56E3636D33A4D36356FA3A9190691A5BAA6FC5B6FD676C08D82A617ACEFBBCDCF6FC2B14955A66D83E2D1E13E92E617587D8B348355"
decryptionKey=
 "25AB4BB356AFF0E1690F87F824540B03D5EF7903B4134521DAAFB61B29889BF1"
validation="SHA1"
decryption="AES"/>
    </system.web>

</configuration>

Teraz już możemy wypróbować stworzony przez nas provider:


Code:
MyProvider user = (MyProvider)Membership.Provider;
            MembershipCreateStatus status;
            user.CreateUser(TextBox1.Text, TextBox2.Text, TextBox3.Text, "", "", false, null, out status,
                "Patryk", "Osowski", DateTime.Now);

Po wywołaniu kodu do bazy zostanie dodany nowy użytkownik.

W taki oto sposób utworzyliśmy kompletny MembershipProvider który możemy wykorzystać w naszym projekcie.

WebParts

WebParts to jeden z ciekawszych mechanizmów dostępnych w ASP.NET WebParts. Pozwala na uzyskanie bardzo ciekawych efektów podczas projektowania interfejsu użytkownika. Dodatkowo daje możliwość dostosowania go przez samego użytkownika.

Co można zaliczyć do WebPartsów? Ilość możliwości wykorzystania ich przy budowie strony zależy tylko od naszej wyobraźni. Niektóre z możliwych zastosowań:
- lista ostatnio dodanych artykułów na stronie
- kalendarz wyświetlający nadchodzące zdarzenia
- lista linków do ciekawych stron
- wyszukiwarka informacji na naszej stronie
- podgląd np. dwóch losowych zdjęć z naszej galerii
- prognoza pogody na nadchodzący dzień
- kursy walut
- notowania giełdowe
oraz wiele innych.

Aby korzystać z dobrodziejstw WebPartsów należałoby się najpierw przyjrzeć strukturze tworzącej szkielet mechanizmu:


WebPartManager - kontrolka wymagana na każdej stronie, która zawiera elementy WebParts. Kontrolka ta jest kontrolką niewizualną, tzn. nie pojawia się jako komponent który widać na stronie. Jej zadaniem jest udostępnienie metod oraz zapewnienie interfejsu działania dla WebPartów.

WebPart - klasa bazowa dla wszystkich WebPartów. Dziedzicząc po niej będziemy tworzyli nasze komponenty.

CatalogPart - zapewnia wizualną część nadzorowania komponentów na stronie. Najczęściej ograniczona do całej witrynie a nie pojedynczej strony.

PageCatalogPart - w działaniu komponent podobny do CatalogPart. Zadaniem kontrolki jest grupowanie komponentów przypisanych do pojedynczej strony (nie całej witryny). Dzięki kontrolce, jeżeli użytkownik zamknie jakiś WebPart, może go przywrócić na innej.

EditorPart - pozwala na modyfikowanie właściwości WebParta

DeclarativeCatalogPart - katalog przechowujący możliwe do dodania komponenty na stronę (lub witrynę)

WebPartZone - wyznacza miejsce gdzie można położyć WebParta

EditorZone - wyznacza miejsce gdzie można umieścić EditorPart

CatalogZone - wyznacza miejsce gdzie można umieścić kontrolkę CatalogPart

Aby można było używać WebPartsów wymagane jest zapisanie informacji o nich w bazie danych. Ma to główną zaletę w postaci zapamiętania układu WebPartsów użytych przez konkretnego użytkownika. Informacje te są standardowo zapisywane w bazie ASPNETDB. Możemy jednak w łatwy sposób przenieść zapis na inną bazę danych.



Tworzenie własnego WebParta
Własny WebPart możemy stworzyć na 3 sposoby:
1. Poprzez stworzenie kontrolki użytkownika - User Control
2. Dziedziczenie po istniejącej kontrolce (np. Labelu czy TextBoxie) 
3. Stworzenie własnej kontrolki Custom Control

Największe możliwości oferuje oczywiście 3 metoda. Jest ona jednak najbardziej pracochłonna. Postaram się więc omówić dwie pierwsze metody. Ostatnią możliwość stosujemy podobnie jak dwie pozostałe a nt. tworzenia własnych kontrolek pisałem już wcześniej artykuł na blogu. A więc zacznijmy.
Rozpoczynamy od stworzenia nowego projektu w Visual Studio. Następnie na formatkę kładziemy komponent WebPartManager. Nie ma on wizualnej postaci dla użytkownika, ale jak pisałem jest wymagany dla każdej strony korzystającej z WebPartsów:






Code:
<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
    CodeBehind="Default.aspx.cs" Inherits="WebApplication5._Default" %>

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
</asp:Content>
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <asp:WebPartManager ID="WebPartManager1" runat="server">

    </asp:WebPartManager>
    <h2>
        Welcome to ASP.NET!
    </h2>
    <p>
        To learn more about ASP.NET visit <a href="http://www.asp.net" title="ASP.NET Website">www.asp.net</a>.
    </p>
    <p>
        You can also find <a href="http://go.microsoft.com/fwlink/?LinkID=152368&amp;clcid=0x409"
            title="MSDN ASP.NET Docs">documentation on ASP.NET at MSDN</a>.
    </p>
    
</asp:Content>

Kolejnymi elementami będą odpowiednie strefy do których użytkownik (jak i my) będziemy mogli dodawać WebParty.
Następnie na formę kładziemy WebPartZone czyli kontrolkę która służy do umieszczania w niej WebParta:


Code:
<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
    CodeBehind="Default.aspx.cs" Inherits="WebApplication5._Default" %>

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
</asp:Content>
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <asp:WebPartManager ID="WebPartManager1" runat="server">

    </asp:WebPartManager>
    <h2>
        Welcome to My Page!
    </h2>
    <asp:WebPartZone ID="WebPartZone1" runat="server" Width="700" Height="500">
    </asp:WebPartZone>
    
</asp:Content>

Po skompilowaniu i uruchomieniu strony możemy otrzymać taki nieprzyjemny widok:



Jest to spowodowane brakiem zainstalowanego SQL Servera w wersji Express na naszej maszynie. Jeżeli tak jak i ja (osoby posiadające SQL Server Express podczas pierwszego uruchomienia mają udogodnienie w postaci automatycznego stworzenia pliku bazy danych) posiadacie inną wersję niż Express (np. Enterprise) należy wcześniej przygotować bazę danych.
W tym celu tworzymy nową pustą bazę danych:






Po stworzeniu bazy należy zintegrować ją z prowiderem odpowiedzialnym za presonalizację. Zrobić możemy to na dwa sposoby: za pomocą narzędzia graficznego jak i tekstowego. Narzędzie tekstowe daje dużo większe możliwości więc użyję go. Samego narzędzia nie będę omawiać, gdyż myślę iż już wystarczająco napisałem o nim w tym artykule. Samo wywołanie tworzące odpowiednią strukturę będzie miało następującą formę:


Następnie w pliku webconfig odpowiednio konfigurujemy sekcję odpowiedzialną za połączenie z bazą danych oraz WebPartami:



Code:
<?xml version="1.0"?>

<!--
  For more information on how to configure your ASP.NET application, please visit
  http://go.microsoft.com/fwlink/?LinkId=169433
  -->

<configuration>
  <connectionStrings>
      <add name="SQLConnString" connectionString="Data Source=localhost;Initial Catalog=test;Integrated Security=True" providerName="System.Data.SqlClient" />
  </connectionStrings>

  <system.web>
    <compilation debug="true" targetFramework="4.0" />

    <authentication mode="Forms">
      <forms loginUrl="~/Account/Login.aspx" timeout="2880" />
    </authentication>

    <membership>
      <providers>
        <clear/>
        <add name="AspNetSqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider" connectionStringName="ApplicationServices"
             enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false"
             maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10"
             applicationName="/" />
      </providers>
    </membership>

    <profile>
      <providers>
        <clear/>
        <add name="AspNetSqlProfileProvider" type="System.Web.Profile.SqlProfileProvider" connectionStringName="ApplicationServices" applicationName="/"/>
      </providers>
    </profile>

    <roleManager enabled="false">
      <providers>
        <clear/>
        <add name="AspNetSqlRoleProvider" type="System.Web.Security.SqlRoleProvider" connectionStringName="ApplicationServices" applicationName="/" />
        <add name="AspNetWindowsTokenRoleProvider" type="System.Web.Security.WindowsTokenRoleProvider" applicationName="/" />
      </providers>
    </roleManager>

    <webParts>
      <personalization defaultProvider="SqlPersonalizationProvider">
        <providers>
          <add name="SqlPersonalizationProvider" type="System.Web.UI.WebControls.WebParts.SqlPersonalizationProvider"
             connectionStringName="SQLConnString" applicationName="/" />
        </providers>
        <authorization>
          <deny users="*" verbs="enterSharedScope" />
          <allow users="*" verbs="modifyState" />
        </authorization>
      </personalization>
    </webParts>
  </system.web>
  
  <system.webServer>
     <modules runAllManagedModulesForAllRequests="true"/>
  </system.webServer>
</configuration>


Teraz po skompilowaniu i uruchomieniu programu powinniśmy zobaczyć:


Przechodzimy do dalszej części tworzenia naszego WebParta.

Tworzymy Web User Control która będzie mieć za zadanie wyświetlanie kalendarza użytkownikowi:










Następnie przeciągamy z Solution nowo stworzoną kontrolkę do strefy:


Następnie uruchamiamy stworzoną aplikację. Po załadowaniu aplikacji widzimy nasz dodatek:


Dodatek ma opcje takie jak możliwość jego minimalizacji oraz zamknięcia. Daje to możliwość użytkownikowi wybrania czy dany komponent chce widzieć czy też nie.

Przedstawione tutaj możliwości to podstawy tworzenia WebPartsów. Istnieje możliwość tworzenia kolekcji WebPartsów z których użytkownik wybierze te, które mu odpowiadają, przenoszenia komponentów pomiędzy odpowiednio wyznaczonymi obszarami strony, jak i możliwość dodawania nowych WebPartsów przez samych użytkowników.

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.

Mapa strony w ASP.NET WebForms

Tworząc zaawansowaną witrynę, zawierającą wiele odnośników i form, należy przemyśleć system nawigacji po stronie. Przedzieranie się poprzez wszystkie elementy menu, podmenu itd. może mocno zirytować użytkownika który odwiedza taką stronę.
Każdy nowy użytkownik potrzebuje centralnego miejsca skąd może dotrzeć w łatwy i szybki sposób do zasobów strony. Dodatkowo stworzenie mapy witryny ułatwi pracę robotą wyszukiwarek internetowych dzięki czemu nasza strona znajdzie się wyżej w wynikach wyszukiwarek
Dla naszej wygody ASP.NET WebForms umożliwia wykonanie tej czynności w szybki i łatwy sposób.
Microsoft dla takich celów przygotował mechanizm zwany Site Map. Mapa strony zawiera hierarchiczną strukturę całej witryny. Dane pobierane są z pliku XML.

Stwórzmy więc prosty projekt, który będzie zawierać odgałęzienia, treść jest w tym momencie nie ważna. Moja przykładowa struktura będzie wyglądała następująco:


Dodajemy więc plik Mapy strony:


W środku pliku standardowo znajduje się poniższa treść:


Code:
<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
    <siteMapNode url="" title=""  description="">
        <siteMapNode url="" title=""  description="" />
        <siteMapNode url="" title=""  description="" />
    </siteMapNode>
</siteMap>

Kolejne elementy mapy definiowane są przez składnię:


Code:
<siteMapNode url="" title=""  description="" />

gdzie:
title - treść wyświetlana użytkownikowi
url - definiuje adres strony na którą ma przenieść użytkownika
description - nieobowiązkowy opis

Tak więc przykładowa implementacja będzie miała postać:


Code:
<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
    <siteMapNode url="" title=""  description="">
        <siteMapNode url="Account\ChangePassword.aspx " title="Zmiana hasła"  description="" />
        <siteMapNode url="Account\ChangePasswordSuccess.aspx" title="Pomyślna zmiana hasła"  description="" />
        <siteMapNode url="Account\Login.aspx" title="Logowanie" description="" />
        <siteMapNode url="Account\Register.aspx" title="Rejestracja" description="" />
        <siteMapNode url="About.aspx" title="O stronie" description="" />
        <siteMapNode url="Categories.aspx" title="Kategorie" description="" />
        <siteMapNode url="Products.aspx" title="Produkty" description="" />
    </siteMapNode>
</siteMap>

Mając plik XML należy przypisać jego zawartość jakiejś kontrolce. Tutaj Microsoft także postanowił ułatwić sprawę i stworzył odpowiednie kontrolki do bindowania treści mapy strony.
Aby wyświetlić dane z pliku XML musimy użyć kontrolki bindującej SiteMapDataSource:


Code:
<asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server" />

Następnie dane możemy wyświetlić w jednej z kontrolek: Menu, TreeView oraz SiteMapPath. Ja użyję kontrolki Menu:


Kod stojący za wyświetlaniem tego menu przedstawia się następująco:


Code:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="SiteStructure.aspx.cs" Inherits="WebApplication3.SiteStructure" %>

<!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></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server" ShowStartingNode="true" />
        <asp:Menu ID="Menu1" runat="server" DataSourceID="SiteMapDataSource1" MaximumDynamicDisplayLevels="5">
        </asp:Menu>
    </div>
    </form>
</body>
</html>

W taki oto sposób osiągnęliśmy zamierzony efekt. ASP.NET webforms oferuje wiele ułatwień dla programistów. Umiejętne wykorzystanie ich może zarówno wzbogacić naszą stronę jak i sprawić że będzie bardziej dostępna w internecie.

Tematy (Skiny) w aplikacji ASP.NET WebForms

Odwiedzając stronę internetową czy też portal w pierwszej kolejności zwracamy na jej wygląd. Każdy chciałby mieć możliwość oglądania czegoś więcej niż dwukolorową stronę. Najlepiej jeszcze gdy użytkownik ma możliwość wyboru schematów kolorów dla strony. oferując kilka różnych skinów strony użytkownik ma możliwość dostosowania kolorystyki do swoich upodobań.

Najprostszym sposobem umożliwiającym zmianę kolorystyki strony jest zastosowanie skinów. Skinów możemy zdefiniować dowolną ilość a odnosimy się do nich przez nazwę. Możemy np. zdefiniować np skin blue oraz red i w zależności od wybranego skinu przez użytkownika załadować odpowiedni.

Przystąpmy więc do implementacji naszego pomysłu:
Stworzymy stronę na której użytkownik za pomocą kontrolki DropDownList może wybrać skin i zaaplikować go.

Tworzenie skinów jest bardzo proste. Najpierw musimy stworzyć specjalny folder App_Themes:


W taki sam sposób dodajemy kolejne tematy. Ja dodałem motyw czerwony i niebieski:


Następnie wypełniamy oba pliki poniższym kodem:
Red:

Code:
<asp:TextBox runat="server" BackColor="Red" BorderColor="DarkRed" Font-Bold="true" ForeColor="Yellow" />

Blue:

Code:
<asp:TextBox runat="server" BackColor="Blue" BorderColor="BlueViolet" Font-Bold="true" ForeColor="Yellow" />

Kod więc ma za zadania zmianę właściwości TextBox-a. Teraz dodatkowo udostępnimy taką możliwość poprzez wybranie z listy odpowiedniego skina. Dodajemy na formatkę DropDownList:


Code:
<asp:DropDownList ID="ddlSkinList" runat="server" AutoPostBack="True">
                    <asp:ListItem Text="Blue" Value="Blue" />
                    <asp:ListItem Text="Red" Value="Red" />
                </asp:DropDownList>

Skin można załadować tylko w momencie wywołania metody Page_PreInit w późniejszych zdarzeniach nie jest już to możliwe. Tak więc tworzymy kod dla tej metody:


Code:
protected void Page_PreInit(object sender, EventArgs e)
        {
            if (Page.Request.Form.Count > 0)
            {
                Page.Theme = Page.Request.Form[5].ToString();
            }
        }

Teraz możemy obserwować efekt:


W przypadku wybrania drugiego skina otrzymamy wynik:


Dzięki wybraniu opcji AutoPostBack zaraz po wybraniu stylu zostanie przeładowana strona i załadowany będzie nowy skin.

Oczywiście zaprezentowany tutaj przykład jest bardzo prosty. Stylizacja przydaje się zwłaszcza w przypadku bardziej zaawansowanych kontrolek - jak np kalendarz, GridView itp.

Mechanizm jest na tyle prosty, że warto poeksperymentować z nim.

ASP.NET WebForms - Podstawy informacje

W tym poście podstawy ASP.NET WebForms. Architektura WebForms umożliwia najszybsze tworzenie stron WWW w technologi .NET (o ile nie najszybsze ze wszystkich mi znanych technologii). Z pewnością jest ona świetna do stron typu wizytówki, proste portale.
Znajduje także zastosowanie w bardziej skomplikowanych projektach, jednak - przynajmniej mi - brakuje w niej oddzielenia kodu od warstwy prezentacji. Jest to właściwie niemożliwe do osiągnięcia. Co za tym idzie trudniej tworzyć automatyczne testy dla naszego produktu.
Dodatkowym obciążeniem jest system zapamiętywania stanu kontrolek - ViewState. Powoduje on zwiększenie ruchu pomiędzy przeglądarką użytkownika i serwerem. Tak czy inaczej Microsoft rozwija tę technologię. WebFormsy posiadają bardzo dużą ilość gotowych kontrolek - które po podpięciu źródła danych oferują bogate możliwości prezentacji danych użytkownikowi.

1. Podstawowe pocjęcia
Na początek trochę podstawowych informacji, związanych głównie z samym protokołem HTTP:
Request - żądanie przez przeglądarkę danych z serwera WWW - za pomocą obiektu tej klasy w ASP można wysłać informacje z ciasteczek, query string itp.
Response - odpowiedź serwera na zapytanie przeglądarki -  pozwala np. na zapis ciasteczek na komputerze użytkownika
GET - metoda w protokole HTTP stosowana przede wszystkim do pobierania danych, tam gdzie użytkownik ma możliwość powrotu do stanu wcześniejszego po pewnym czasie poprzez zdefiniowany adres
POST - metoda w HTTP pozwalająca na przesyłanie danych (najczęściej z formularza)
IsPostBack - metoda w ASP.NET pozwalająca na sprawdzenie czy jest to pierwsze wywołanie strony, czy też PostBack - czyli wysłanie strony do serwera (np odświeżenie przez użytkownika za pomocą klawisza F5):


Code:
protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                //Do something
            }
        }

Właściwość tę najczęściej wykorzystujemy w przypadku kiedy w metodzie Load strony podpinamy jakieś źródło danych czy też wypełniamy kontrolki danymi. Dzięki temu unikamy zdublowaniu danych w kontrolkach.

Zapamiętywanie informacji
W ASP.NET WebForms mamy podobnie jak w MVC i innych technologiach internetowych możliwość zapamiętywania danych pomiędzy kolejnymi przejściami między stronami. Mechanizmów jest kilka, a dzielą się one na dwie grupy:
działające po stronie serwera:
+ bezpieczeństwo
+ nie zwiększa ilości przesyłanych danych
- zużycie zasobów serwera (pamięć, procesor)
działające po stronie klienta:
+ brak obciążenia serwera
- zwiększenie zużycia pasma
- mniejsze bezpieczeństwo przesyłanych danych 

Pliki Cookie:
  

Code:
//1. Sprawdzamy czy na komputerze użytkownika jest już plik cookie
            if (Request.Cookies["name"] != null)
            {
                //2. Przypsianie zmiennej zawartości pliku cookie
                string valueFromCookie = Request.Cookies["name"].Value;
            }
            else
            {
                //3. Zapisujemy nowy plik cookie na komputerze użytkownika jeżeli
                //go tam jeszcze nie ma
                Response.Cookies["name"].Value = "Value of Cookie";
                //Jeżeli nie ustawimy właściwości Expires plik cookie zostanie usunięty
                //po wyłączeniu przeglądarki użytkwonika
                //Uwaga - aby usunać plik cookie należy mu przypisać datę wcześniejszą od aktualnej
                //nie mamy fizycznego dostępu do dysku użytkownika!
                Response.Cookies["name"].Expires = DateTime.Now.AddDays(20);
            }

QueryString:
Query string możemy zdefiniować w następujący sposób:
adres strony/ + querystring
Tak więc QueryString to nic innego jak parametry które są przesyłane pomiędzy wywołaniami za pomocą adresu www. Jeżeli chodzi o sam adres www musimy sami go stworzyć. Czytaniem wartości QueryString zajmuje się odpowiednia klasa, która przyporządkowuje wartości do słownika:
Dla adresu http://localhost:56505/Default.aspx?FirstName=Jacek&LastName=Kowalski użyjemy:


Code:
string firstName = Request.QueryString["FirstName"];
                string lastName = Request.QueryString["LastName"];

Jak więc widać QueryString tworzymy w następujący sposób:
po adresie strony stawiamy pytajnik - ?
klucze oddzielamy od siebie za pomocą znaku ampresanda &
klucze zapisujemy w postaci klucz=wartosc

W każdym przypadku powinniśmy parsować to co otrzymamy od użytkownika. Zabezpieczamy się tym samym przed uruchomieniem złośliwego oprogramowania. Aby przeparsować dane od użytkownika możemy użyć metody HtmlEncode:


Code:
Server.HtmlEncode(firstName);

ViewState:
Wartości kontrolek na formie, aby zostały odtworzone po np. przeładowaniu formatki, są zapisywane do ukrytego pola - ViewState. Domyślnie wartość każdej kontrolki jest zapamiętywana w ViewState. Można jednak wyłączyć zapamiętywanie dla tych kontrolek dla których wiemy że nie jest to potrzebne.

Session:
Jest to sposób na przechowywanie danych po stronie serwera. Dla każdego użytkownika tworzony jest nowy taki obiekt i w zależności od ustawień aplikacji trzymany przez pewien określony czas:


Code:
if (Session["Login"] != null)
                {
                    //Do something
                }
                else
                {
                    Session["Login"] = "Guest";
                }

Należy pamiętać, że każda zmienna sesji obciąża serwer i powoduje rezerwację pamięci dla każdego użytkownika, który w danym czasie wszedł na naszą witrynę.

Application
Podobnie jak w przypadku sesji jest to mechanizm działający po stronie serwera. Różnica między zmienną sesyjną a aplikacji jest taka, iż zmienna aplikacji jest tylko jedna tzn. tworzy się w momencie pierwszego wejścia na stronę, a kończy swój okres życia w momencie wyłączenie całkowicie aplikacji.


Code:
if (Application["VisitorCount"] != null)
                {
                    //Do something
                }
                else
                {
                    Application["VisitorCount"] = ((int)Application["VisitorCount"]) + 1;
                }