ASP.Net e Pattern Observer in Pratica – Quarta Parte

In questo post, come promesso in ASP.Net e Pattern Observer in Pratica – Terza Parte, utilizzeremo il pattern observer per assolvere al requisito prensentato nel primo post della serie ASP.Net e Pattern Observer in Pratica.
Allaciate le cinture che questa volta saremo un po’ lunghetti 🙂

Ricordiamo il Requisito

Implementazione di una pagina di ricerca dove sono presenti alcuni web user controls tra i quali uno in particolare si occupa di raccogliere l’input dell’utente per effettuare una ricerca tramite la pressione di un pulsante apposito (presente sullo stesso user control), l’operazione di ricerca deve in qualche modo essere notificata agli altri user controls presenti sulla stessa pagina che utilizzando gli stessi valori inseriti nel primo controllo dovranno filtrare e renderizzare i dati di dettaglio.

Ciò che implementeremo

Una serie di classi necessarie alla rappresentazione di un repository di Clienti, un unica pagina contenente due web user control uno per gestire l’operazione di ricerca (il pubblicatore) l’altro per la rappresentazione a video della lista filtrate di clienti (il sottoscrittore). Il controllo web che presenterà i dati filtrati riceverà da parte del controllo di ricerca una notifica ogni volta che l’utente modificherà i valori del filtro ed effettuerà una nuova ricerca. Insieme alla notifica viaggeranno anche i valori del filtro impostato da un controllo all’altro, il tutto senza scomodare in alcun modo la sessione.

Di seguito la screenshot dell’interfaccia web

DesignPattern - Esempio del pattern Observer

Per prima cosa andiamo a definire la classe “Customer”, che rappresenta il nostro modello

namespace DesingPattern.Sample.Observer.Model
{
    /// <summary>
    /// Rappresenta l'entità Cliente.
    /// </summary>
    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Surname { get; set; }
        public string City { get; set; }
    }
}

e la relativa “CustomerRepository” (che tramite alcuni metodi statici, vi ricordo che siamo in un esempio, ci fornirà la lista dei Clienti):

namespace DesingPattern.Sample.Observer.Data
{
    /// <summary>
    /// Rappresenta il repository per le entità Cliente.
    /// </summary>
    public class CustomerRepository
    {
        /// <summary>
        /// Restituisce la lista completa di tutti i Clienti.
        /// </summary>
        /// <returns></returns>
        public static IQueryable<Customer> GetAllCustomers()
        {
            List<Customer> customers = new List<Customer>();
            customers.Add(new Customer() { 
                Id = 1, 
                Surname = "Santaniello", 
                Name = "Ernesto", 
                City = "Napoli" 
            });
            customers.Add(new Customer() { 
                Id = 2, 
                Surname = "Tispo", 
                Name = "Armando", 
                City = "Napoli" 
            });
            // ecc..
            return customers.AsQueryable().OrderBy(c => c.Surname);
        }

        /// <summary>
        /// Restituisce una lista di clienti filtrata.
        /// </summary>
        /// <param name="filter">Rappresenta il filtro di ricerca.</param>
        /// <returns></returns>
        public static IQueryable<Customer> GetCustomers(CustomerFilter filter)
        {
            if (filter is NullCustomerFilter)
                return CustomerRepository.GetAllCustomers();

            if(filter.UseLikeFilter)
                return CustomerRepository.GetAllCustomers().Where(c =>
                        (string.IsNullOrEmpty(filter.Name) || c.Name.ToLower().Contains(filter.Name.ToLower()))
                        && (string.IsNullOrEmpty(filter.Surname) || c.Surname.ToLower().Contains(filter.Surname.ToLower()))
                        && (string.IsNullOrEmpty(filter.City) || c.City.ToLower().Contains(filter.City.ToLower()))
                    );
            else
                return CustomerRepository.GetAllCustomers().Where(c =>
                    (string.IsNullOrEmpty(filter.Name) || c.Name.Equals(filter.Name, StringComparison.OrdinalIgnoreCase))
                    && (string.IsNullOrEmpty(filter.Surname) || c.Surname.Equals(filter.Surname, StringComparison.OrdinalIgnoreCase))
                    && (string.IsNullOrEmpty(filter.City) || c.City.Equals(filter.City, StringComparison.OrdinalIgnoreCase))
                );
        }
    }
}

La classe “CustomerRepository” nel metodo “GetCustomers” accetta in ingresso un oggetto di tipo “CustomerFilter” (utilizzato per aggregare le informazioni di filtro) implementata come segue

namespace DesingPattern.Sample.Observer.Model
{
    /// <summary>
    /// Rappresenta il filtro di ricerca per i Clienti.
    /// </summary>
    public class CustomerFilter
    {
        public string Name { get; set; }
        public string Surname { get; set; }
        public string City { get; set; }
        public bool UseLikeFilter { get; set; }
    }

inoltre, utilizzando le nozioni alla base del pattern “NullObject” implementiamo anche la classe “NullCustomerFilter” che in questo caso si riduce ad essere una classe vuota che eredita da “CustomerFilter”

namespace DesingPattern.Sample.Observer.Model
{
    /// <summary>
    /// Rappresenta il filtro vuoto di ricerca per i Clienti.
    /// </summary>
    public class NullCustomerFilter : CustomerFilter
    {
    }
}

L’implementazione d’esempio del pattern observer l’abbiamo già vista, ma rispetto a quella dobbiamo aggiungere qualcosa che permetta ai sottoscrittori di ricevere dei dati durante la fase di notifica. A tale scopo modifichiamo la firma del metodo “Update” aggiungendo a questa un parametro in input (del tipo a noi necessario) così da poter passare le informazioni al sottoscrittore durante la chiamata al metodo “Notify” del pubblicatore:

Nota
E’ di estrema importanza comprendere che modificare la firma del metodo Update non vuol dire modificare la struttura del Pattern Observer, i pattern identificano il disegno della soluzione da implementare e non la loro esatta implementazione.

Se a questo punto siamo abbastanza furbi da utilizzare i generics possiamo costruire un interfaccia generica per qualsiasi tipo di sottoscrittore in questo modo:

namespace DesingPattern.Sample.Observer.Interfaces
{
    /// <summary>
    /// Interfaccia da implementare in tutti i sottoscrittori.
    /// </summary>
    public interface IObserver<T>
    {
        /// <summary>
        /// Aggiorna lo stato interno del sottoscrittore quando riceve 
        /// l'evento di notifica dal pubblicatore.
        /// </summary>
        /// <param name="data">Dati da passare durante la notifica.</param>
        void Update(T data);
    }
}

Nota
Attenzione a partire dalla versione 4.0 del Framework .Net l’interfaccia <a title="Interfaccia System.IObserver” href=”http://msdn.microsoft.com/it-it/library/dd783449(v=vs.100).aspx” target=”_blank”>IObserver<T> è già presente all’interno del namespace System quindi attenti agli using.

Allo stesso modo, sempre utilizzando i generics, modificheremo l’interfaccia ISubject come segue:

namespace DesingPattern.Sample.Observer.Interfaces
{
    /// <summary>
    /// Interfaccia da implementare sul pubblicatore.
    /// </summary>
    interface ISubject<T>
    {
        /// <summary>
        /// Registra un sottoscrittore all'evento di notifica. 
        /// </summary>
        /// <param name="o">Interfaccia che identifica un Sottoscrittore.</param>
        void Attach(IObserver<T> o);

        /// <summary>
        /// Rimuove un sottoscrittore dall'evento di notifica.
        /// </summary>
        /// <param name="o">Interfaccia che identifica un Sottoscrittore.</param>
        void Detach(IObserver<T> o);

        /// <summary>
        /// Notifica a tutti i sottoscrittori il cambio di stato.
        /// </summary>
        void Notify();
    }
}

A questo punto andiamo ad implementare una classe astratta che rappresenti la versione base del controllo web di ricerca che erediti da System.Web.UI.Usercontrol

namespace DesingPattern.Sample.Observer.WebUserControls
{
    /// <summary>
    /// Controllo di filtro base.
    /// </summary>
    /// <typeparam name="T">Tipo di filtro.</typeparam>
    public abstract class FilterControlBase<T> : System.Web.UI.UserControl, Interfaces.ISubject<T>
    {
        private T _filter;
        /// <summary>
        /// Lista interna degli sottoscrittori registrati all'evento di notifica.
        /// </summary>
        private List<Interfaces.IObserver<T>> _observers = new List<Interfaces.IObserver<T>>();

        /// <summary>
        /// Restituisce o imposta il valore del filtro.
        /// </summary>
        public T Filter
        {
            get { return _filter; }
            set
            {
                _filter = value;
                // Ogni volta che il valore del filtro cambia
                // viene spedita la notifica ai sottoscrittori.
                this.Notify();
            }
        }

        /// <summary>
        /// Sottoscrive un osservatore. 
        /// </summary>
        /// <param name="o">Riferimento al sottoscrittore.</param>
        public void Attach(Interfaces.IObserver<T> o)
        {
            _observers.Add(o);
        }

        /// <summary>
        /// Rimuove un osservatore.
        /// </summary>
        /// <param name="o">Riferimento al sottoscrittore</param>
        public void Detach(Interfaces.IObserver<T> o)
        {
            _observers.Remove(o);
        }

        /// <summary>
        /// Notifica, a tutti gli osservatori sottoscritti, 
        /// che il valore del filtro è stato modificato.
        /// </summary>
        public void Notify()
        {
            foreach (Interfaces.IObserver<T> o in _observers)
            {
                o.Update(this.Filter);
            }
        }
    }
}

La cose da notare nell’implementazione della classe “FilterControlBase” sono:

  1. La presenza della proporty “Filter” tipizzata tramite i generics;
  2. Il Setter della property “Filter” che invoca il metodo “Notify” ogni volta che viene cambiato lo stato del filtro;
  3. Nel metodo “Notify” a tutti i sottoscrittori viene notificato il valore del nuovo filtro.

Ormai ci siamo, le ultime cose di cui abbiamo bisogno sono i due web user control e la pagina che li ospiterà.

Il controllo Search.ascx

Il controllo Search.ascx identifica il nostro pubblicatore.
Nel markup inseriamo una serie di controlli per la raccolta dell’input utente e due pulsanti uno per effettuare la ricerca l’altro per ripulire la form.

<%@ Control Language="C#" 
    AutoEventWireup="true" 
    CodeBehind="Search.ascx.cs" 
    Inherits="DesingPattern.Sample.Observer.WebUserControls.Search" %>

<fieldset id="search">
    <legend>Search Fields</legend>
    <asp:CheckBox ID="chkUseLikeFilter" runat="server" /> Use Like Mode
    <div class="filters break">
        <div class="field">
            <asp:Label ID="lblSurname" runat="server" 
                       AssociatedControlID="txtSurname" Text="Surname" />
            <asp:TextBox ID="txtSurname" runat="server" />
        </div>
        <div class="field">
            <asp:Label ID="lblName" runat="server" 
                       AssociatedControlID="txtName" Text="Name" />
            <asp:TextBox ID="txtName" runat="server" />
        </div>
        <div class="field">
            <asp:Label ID="lblCity" runat="server" 
                       AssociatedControlID="txtCity" Text="City" />
            <asp:TextBox ID="txtCity" runat="server" />
        </div>
        <div class="buttons break">
            <asp:Button ID="btnApplyFilter" runat="server" 
                        Text="Apply Filter" OnClick="btnApplyFilter_Click" />
            <asp:Button ID="btnClearFilter" runat="server" 
                        Text="Clear Filter" OnClick="btnClearFilter_Click" />
        </div>
    </div>
</fieldset>

Il codice del controllo contiene i soli gestori d’evento dei due pulsanti dove vengono reimpostati i filtri. Inoltre ereditando dalla classe astratta “FilterControlBase” nel momento in cui viene reimpostata la property “Filter” viengono anche automaticamente notificati ai sottoscrittori i nuovi valori di filtro.

namespace DesingPattern.Sample.Observer.WebUserControls
{
    public partial class Search : WebUserControls.FilterControlBase<CustomerFilter>
    {
        protected void btnApplyFilter_Click(object sender, EventArgs e)
        {
            // Alla pressione del pulsante di ricerca sul controllo basterà 
            // aggiornare il valore del filtro intrinsecamente legato al pubblicatore.
            this.Filter = new CustomerFilter() { 
                UseLikeFilter = chkUseLikeFilter.Checked,
                Surname = txtSurname.Text,
                Name = txtName.Text,
                City = txtCity.Text
            };
        }

        protected void btnClearFilter_Click(object sender, EventArgs e)
        {
            // Alla pressione del pulsante Clear sul controllo basterà 
            // aggiornare il valore del filtro intrinsecamente legato al pubblicatore.
            this.Filter = new NullCustomerFilter();

            // Ripuliamo inoltre i controlli di input
            chkUseLikeFilter.Checked = false;
            txtSurname.Text = string.Empty;
            txtName.Text = string.Empty;
            txtCity.Text = string.Empty;
        }
    }
}

Il controllo SearchResults.ascx

Il controllo SearchResults.ascx identifica invece il nostro sottoscrittore.
Nella parte di marckup inseriamo un semplice repeater, come segue

<%@ Control Language="C#" 
    AutoEventWireup="true" 
    CodeBehind="SearchResults.ascx.cs" 
    Inherits="DesingPattern.Sample.Observer.WebUserControls.SearchResults" %>

<asp:Repeater ID="rptResults" runat="server">
    <HeaderTemplate>
        <table>
            <thead>
                <tr>
                    <th>ID</th>
                    <th>Surname</th>
                    <th>Name</th>
                    <th>City</th>
                </tr>
            </thead>
            <tbody>
    </HeaderTemplate>
    <ItemTemplate>
        <tr>
            <td><%#Eval("Id")%></td>
            <td><%#Eval("Surname")%></td>
            <td><%#Eval("Name")%></td>
            <td><%#Eval("City")%></td>
        </tr>
    </ItemTemplate>
    <FooterTemplate>
            </tbody>
        </table>
    </FooterTemplate>
</asp:Repeater>

mentre nella parte di codice inseriamo

  1. Il gestore di evento relativo al Load del controllo, che si occuperà di bindare la lista dei clienti al repeater durante il primo caricamento (ovvero quando la pagina viene caricata per la prima volta ed il filtro non è stato ancora impostato);
  2. Il metodo Update, relativo all’implementazione dell’interfaccia “Interfaces.IObserver” (che viene invocato dal publicatore durante l’esecuzione del metodo “Notify”) nel quale viene filtrata la lista dei clienti in base ai valori di filtro passati durante la notifica del pubblicatore.
namespace DesingPattern.Sample.Observer.WebUserControls
{
    public partial class SearchResults : System.Web.UI.UserControl, 
                                         Interfaces.IObserver<CustomerFilter>
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                // Binda i dati non filtrati sul repeater per la prima visualizzazione.
                rptResults.DataSource = CustomerRepository.GetAllCustomers();
                rptResults.DataBind();
            }
        }

        public void Update(CustomerFilter data)
        {
            // Binda i dati filtrati sul repeater.
            rptResults.DataSource = CustomerRepository.GetCustomers(data);
            rptResults.DataBind();
        }
    }
}

La pagina web contenitore

In fine nella pagina Default.aspx inseriamo i due web controls

<%@ Page Language="C#" AutoEventWireup="true" 
         CodeBehind="Default.aspx.cs" 
         Inherits="DesingPattern.Sample.Observer.Default" %>
<%@ Register src="WebUserControls/Search.ascx" 
             tagname="Search" tagprefix="uc" %>
<%@ Register src="WebUserControls/SearchResults.ascx" 
             TagName="SearchResults" tagprefix="uc" %>

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>ASP.Net &amp; Pattern Observer in Practice</title>
</head>
<body>
    <form id="form1" runat="server">
        <h1>ASP.Net &amp; Pattern Observer in Practice</h1>
        <uc:Search ID="ucSearch" runat="server" />
        <uc:SearchResults ID="ucSearchResults" runat="server" />
    </form>
</body>
</html>

e nel code-behind registriamo il sottoscrittore alla notifica nel gestore d’evento “Init” della pagina

namespace DesingPattern.Sample.Observer
{
    public partial class Default : System.Web.UI.Page
    {
        protected void Page_Init(object sender, EventArgs e)
        {
            // Registra il componente "ucSearchResults" alla notifica.
            ucSearch.Attach(ucSearchResults);
        }
    }
}

Bene se hai resistito fino a questo punto ti meriti di scaricare il progetto di esempio, ma prima di lasciarti voglio aggiungere ancora uno spunto..

Immagina che, nell’esempio implementato dopo l’operazione di ricerca, effettuando un click su una delle righe presenti nella griglia dei clienti tu debba visualizzare un dettaglio più corposo dei dati relativi al cliente selezionato… 😉
Chi vieta che un sottoscrittore (in questo caso SearchResults.ascx) sia contemporaneamente anche un pubblicaotore?

Sperando di averti stuzzicato abbasanza ti lascio come esercizio questo unltimo punto, ormai dovresti essere perfettamente a tuo agio con il pattern observer!!!

Annunci

Rispondi

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione / Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione / Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione / Modifica )

Google+ photo

Stai commentando usando il tuo account Google+. Chiudi sessione / Modifica )

Connessione a %s...