Tutorial ASP.NET 2.0 - Parte quarta: modificare dati
a cura di Antonio Catucci e Mario De Ghetto (requisiti: conoscenza di ADO.Net e SQL)

Premessa
Nell'articolo precedente abbiamo visto come visualizzare dei dati sfruttando una GridView collegata a una ObjectDataSource. L'operazione è stata semplice ma, alla fine, il guadagno è di poche righe di codice. In questo articolo analizzeremo più in dettaglio l'ObjectDataSource. In particolare vedremo come sia possibile modificare i dati in maniera più semplice rispetto al passato.

Come funziona l'ObjectDataSource
Come già descritto brevemente nel precedente articolo, l'ObjectDataSource altro non è che una classe "proxy" usata per far comunicare un controllo con una base dati generalmente rappresentata da una classe business. Una volta scelto l'oggetto da usare per l'accesso ai dati, si impostano le proprietà SelectMethod, InsertMethod, UpdateMethod e DeleteMethod che servono, rispettivamente, per selezionare, inserire, aggiornare e cancellare dati.

In questo articolo e nei successivi vedremo come funzionano in dettaglio queste proprietà applicate ai nuovi controlli data GridView, DetailView e FormView.

Prima di iniziare prepariamo il progetto di esempio realizzato fino a questo momento creando una nuova cartella denominata 4_ModificareDati all'interno della quale inseriremo le pagine che creeremo durante l'articolo.

Modificare i dati
La modifica dei dati visualizzati in una GridView è diventata molto più semplice in ASP.NET 2.0 rispetto a quanto accadeva nella versione 1.x. Infatti, grazie al controllo ObjectDataSource è possibile risparmiare un bel po' di codice in virtù del binding bidirezionale introdotto proprio nella 2.0. In precedenza, infatti, la seccatura più grossa era proprio quella di dover raccogliere i dati dal DataGrid e passarli manualmente al Data Layer (o direttamente al database). Con l'ObjectDataSource, invece, questo meccanismo è stato automatizzato e semplificato.

Il primo passo è creare il metodo di aggiornamento nel Business Layer dei clienti. Questa è la versione modificata della classe Customer:

Public Class Customer

  ' [omissis]

  Public Shared Function UpdateCustomer(ByVal data As Customer) As Boolean
    If data Is Nothing Then Return False

    Dim dt As Northwind.CustomersDataTable = _
                                           Customer.CustomerProvider.GetCustomerById(data.CustomerID)
    If dt.Rows.Count = 0 Then Return False

    Dim row As Northwind.CustomersRow = dt.Rows(0)
    With row
      .CompanyName = data.CompanyName
      .ContactName = data.ContactName
      .ContactTitle = data.ContactTitle
      .Address = data.Address
      .PostalCode = data.PostalCode
      .City = data.City
      .Country = data.Country
      ._Region = data.Region
      .Fax = data.Fax
      .Phone = data.Phone
    End With
    Return Customer.CustomerProvider.Update(row) > 0
  End Function

End Class

Molto semplicemente si passano i dati al provider CustomerProvider mediante il metodo UpdateCustomer che esegue fisicamente l'operazione di aggiornamento nel dabatase.

Ora creiamo una nuova pagina denominata "ModificaCliente.aspx" nella cartella 4_ModificareDati e aggiungiamo un ObjectDataSource (odsCustomers) ed una GridView (gvCustomers) per consentire la visualizzazione e modifica dei clienti. Configuriamo l'ObjectDataSource impostando la SELECT sul metodo GetCustomers (come abbiamo già visto nel precedente articolo). Oltre al metodo SELECT è possibile impostare altri tre metodi, ovvero l'UPDATE, l'INSERT e il DELETE. A noi interessa aggiornare i dati quindi configuriamo il metodo UPDATE agganciandolo al metodo appena creato UpdateCustomer:

Il markup generato alla conlusione del wizard è il seguente:

    <asp:ObjectDataSource ID="odsCustomers" runat="server"
        DataObjectTypeName="VBTT.Tutorial.BLL.Customer"
        TypeName="VBTT.Tutorial.BLL.Customer"
        SelectMethod="GetCustomers" 
        UpdateMethod="UpdateCustomer" />

Il TypeName indica la classe che contiene i metodi da invocare, il DataObjectTypeName è il tipo di oggetto da utilizzare per interagire con i metodi (nel nostro caso un tipo Customer). Quando viene specificato DataObjectTypeName bisogna che vengano soddisfatti alcuni requisiti:

  1. L'oggetto specificato deve avere almeno un costruttore pubblico senza parametri (per consentirne la creazione);
  2. I campi da visualizzare devono essere delle Property Get/Set.

Ora colleghiamo la nostra ObjectDataSource alla GridView configurandola come abbiamo fatto nel precedente articolo, cioè visualizzando i campi CustomerID, CompanyName, Address, City e Country.
Per consentire la modifica dei dati all'interno della griglia è sufficiente abilitare l'opzione "Enable Editing":

Automaticamente viene aggiunta una nuova colonna di tipo CommandField contenente il link Edit che gestisce tutto il meccanismo di rendering in fase di modifica dei dati. Il codice markup generato dalla GridView è il seguente:

    <asp:GridView ID="gvCustomers" AutoGenerateColumns="False" runat="server" 
        DataKeyNames="CustomerID" DataSourceID="odsCustomers" Width="700px">
            <Columns>
                <asp:CommandField ShowEditButton="True" 
                    EditText="Modifica" 
                    UpdateText="Ok" 
                    CancelText="Annulla" 
                    DeleteText="Elimina" 
                    ShowDeleteButton="True" >
                    <ItemStyle Width="80px" />
                </asp:CommandField>
                <asp:BoundField DataField="CustomerID" HeaderText="Codice" 
                    ReadOnly="True" SortExpression="CustomerID">
                    <HeaderStyle Width="40px" />
                </asp:BoundField>
                <asp:BoundField DataField="CompanyName" HeaderText="Ragione sociale" 
                    SortExpression="CompanyName" >
                    <HeaderStyle HorizontalAlign="Left" />
                </asp:BoundField>
                <asp:BoundField DataField="Address" HeaderText="Indirizzo" 
                    SortExpression="Address">
                    <HeaderStyle HorizontalAlign="Left" Width="200px" />
                </asp:BoundField>
                <asp:BoundField DataField="City" HeaderText="Città" SortExpression="City">
                    <HeaderStyle HorizontalAlign="Left" Width="100px" />
                </asp:BoundField>
                <asp:BoundField DataField="Country" HeaderText="Stato" SortExpression="Country">
                    <HeaderStyle HorizontalAlign="Left" Width="100px" />
                </asp:BoundField>
            </Columns>
        </asp:GridView>

E' possibile personalizzare la colonna di edit impostando le proprietà EditText, UpdateText e CancelText per indicare, rispettivamente, il testo dei comandi di modifica, salvataggio e annullo.

Una proprietà importante è DataKeysName. Si tratta di una proprietà che accetta uno o più valori separati da una virgola, corrispondente ai campi da utilizzare per identificare univocamente una riga. Questa proprietà è fondamentale quando si gestiscono operazioni di modifica ai dati, perché consente all'oggetto datasource di individuare la riga che si sta modificando. Nel nostro esempio impostiamo il valore con CustomerID. Infine, notate la colonna Codice sia stata resa non modificabile impostando la proprietà ReadOnly a True.

Eseguiamo la pagina e questo è il risultato se si clicca sul link Modifica:

La GridView mostra delle TextBox per ciascuna colonna, tranne per quelle che hanno la proprietà ReadOnly a True come il campo CustomerID, e visualizzerà i comandi Salva e Annulla per confermare o annullare l'operazione. Ancora una volta è bene sottolineare il fatto che non è stato ancora scritto del codice mentre con la DataGrid è necessario intervenire nell'evento EditCommand() per attivare la modalità di editing.

Però c'è un problema. Se proviamo a salvare una modifica viene sollevata un'eccezione come questa:

Perché? Perché l'ObjectDataSource tenta di impostare una proprietà in sola lettura, in questo caso CustomerID. Il comando di Update impostato nella datasource prevede un parametro di tipo Customer che viene creato e valorizzato al salvataggio dei dati e passato al metodo UpdateCustomer della classe business Customer. La GridView imposta tutte le proprietà dell'oggetto Customer con i valori della griglia, limitatamente alle colonne in binding, e quando tenta di impostare CustomerID va in errore, essendo una proprietà ReadOnly.

Esiste anche un secondo problema. La griglia imposta solo le colonne visualizzate valorizzando tutte le altre con il valore di default in base al tipo (quindi Empty per le stringhe, 0 per gli Integer e così via) ottenendo, così un effetto poco piacevole: la cancellazione di eventuali valori esistenti nel database. Nel nostro esempio perderemmo i valori del telefono, fax, ecc perché non visualizzati nella GridView e quindi verrebbero impostati a String.Empty!

Una soluzione ad entrambi i problemi consiste nell'aggiungere un overload al metodo UpdateCustomer della classe business passando solo i dati che si intendono modificare:

        Public Shared Function UpdateCustomer(ByVal customerID As String, _
                                                ByVal companyName As String, _
                                                ByVal address As String, _
                                                ByVal city As String, _
                                                ByVal country As String) As Boolean

            If String.IsNullOrEmpty(customerID) Then Return False

            Dim c As Customer = Customer.GetCustomer(customerID)
            With c
                .CompanyName = companyName
                .Address = address
                .City = city
                .Country = country
            End With

            Return c.Update
        End Function

A questo punto modifichiamo il controllo ObjectDataSource perché utilizzi questo overload definendo i parametri da passare:

    <asp:ObjectDataSource ID="odsCustomers" runat="server" 
        TypeName="VBTT.Tutorial.BLL.Customer"
        SelectMethod="GetCustomers" 
        UpdateMethod="UpdateCustomer">
        <UpdateParameters>
            <asp:Parameter Name="customerID" Type="String" />
            <asp:Parameter Name="companyName" Type="String" />
            <asp:Parameter Name="address" Type="String" />
            <asp:Parameter Name="city" Type="String" />
            <asp:Parameter Name="country" Type="String" />
        </UpdateParameters>
    </asp:ObjectDataSource>

A differenza di prima, viene utilizzata la collection contenente l'elenco dei parametri da passare al metodo UpdateCustomer. Per ciascun comando XxxxxMethod è prevista una collection XxxxParameters per la definizione dei parametri da passare al metodo. Inoltre, notate la scomparsa della proprietà DataObjectTypeName, dovuta al fatto che questo valore, se definito, ha la precedenza sui parametri. La definizione dei parametri può essere fatta anche con l'aiuto dell'editor Parameter Collection Editor raggiungibile dalla finestra delle proprietà dell'oggetto:

L'utilizzo dei parametri, anziché di un oggetto, implica che la GridView debba contenere un numero di colonne modificabili pari al numero dei parametri per evitare che venga sollevata l'eccezione InvalidOperationException dovuto al fatto che non esiste nessun metodo con quel numero di parametri. Tuttavia è possibile visualizzare dei campi supplementari, purchè siano ReadOnly, in modo tale che vengano ignorati in fase di modifica.

Abbiamo, ora, una griglia che ci consente di modificare i dati, senza aver scritto ancora del codice. Questo perché è la griglia che fa tutte le operazioni per noi, ovvero riprodurre la riga visualizzando delle TextBox per le colonne modificabili, valorizzare i parametri nella collection UpdateParameters e rifare il binding con la chiamata del metodo SelectMethod per visualizzare i dati aggiornati.

Eliminazione di una riga
Vediamo come aggiungere il supporto per la cancellazione di una riga dalla griglia. Il procedimento è molto simile alla modifica, ovvero impostando il comando DELETE dell'ObjectDataSource come segue:

    <asp:ObjectDataSource ID="odsCustomers" runat="server" 
        TypeName="VBTT.Tutorial.BLL.Customer"
        SelectMethod="GetCustomers" 
        UpdateMethod="UpdateCustomer" 
        DeleteMethod="DeleteCustomerById">
        <UpdateParameters>
            <asp:Parameter Name="customerID" Type="String" />
            <asp:Parameter Name="companyName" Type="String" />
            <asp:Parameter Name="address" Type="String" />
            <asp:Parameter Name="city" Type="String" />
            <asp:Parameter Name="country" Type="String" />
        </UpdateParameters>
        <DeleteParameters>
            <asp:Parameter Name="customerId" Type="string" />
        </DeleteParameters>
    </asp:ObjectDataSource>

Il metodo per la cancellazione di un cliente è DeleteCustomerById che, come il nome lascia intendere, accetta il codice del cliente da eliminare. Questo implica l'aggiunta del parametro nell'ObjectDataSource mediante la collection DeleteParamters.

L'ultimo passo è abilitare l'operazione di cancellazione nella GridView abilitando l'opzione Enable Deleting attraverso lo SmartTag del controllo:

Il codice markup generato si traduce nell'impostare le proprietà ShowDeleteButton a True e DeleteText con il nome del button (si mostra solo la parte modificata):

    <asp:GridView ID="gvCustomers" AutoGenerateColumns="False" runat="server" DataKeyNames="CustomerID" 
        DataSourceID="odsCustomers" Width="700px">
            <Columns>
                <asp:CommandField ShowEditButton="True" 
                    EditText="Modifica" 
                    UpdateText="Ok" 
                    CancelText="Annulla" 
                    DeleteText="Elimina" ShowDeleteButton="True" >
                    <ItemStyle Width="80px" />
                </asp:CommandField>

Per brevità si omette il codice del metodo DeleteCustomerById (del resto molto intuitivo e comunque presente nel codice a corredo).

Ancora una volta mi preme sottolineare come sia possibile aggiungere delle funzionalità più o meno complesse senza scrivere codice. Infatti tutto il processo di comunicazione tra la GridView e la fonte dati sottostante è assolutamente trasparente ed automatica.

Provando ad eseguire la pagina ModificaDati.aspx con le nuove modifiche e cancellando una riga non otteniamo il risultato sperato, ma un bell'errore come questo:

Il motivo è presto spiegato. Nella tabella Orders del database Northwind esiste un vincolo con la tabella Customers che impedisce di avere ordini senza il relativo cliente in anagrafica (come è lecito che sia), pertanto, quando si tenta di eliminare un cliente con almeno un ordine effettuato, si ottiene l'errore mostrato in precedenza.

Vedremo negli articoli successivi come gestire gli errori a run time, per il momento possiamo provare l'eliminazione di una riga con gli unici due clienti che non hanno ordini, ossia PARIS e FISSA. In alternativa potete rimuovere il vincolo dal database per fare delle prove (naturalmente fate prima una copia...).

Aggiungere un messaggio di conferma
Concludiamo l'articolo mostrando come aggiungere un messaggio di conferma prima di eliminare definitivamente un cliente (cosa che dovrebbe avvenire normalmente in un'applicazione reale!). Esistono diversi modi per ottenere il risultato e noi mostreremo quello più semplice ed adatto al nostro esempio.

Utilizzeremo la funzione javascript confirm() che visualizza una message box modale con i tasti OK e ANNULLA e restituisce rispettivamente True o False a seconda del tasto premuto. Questa funzione dovrà essere agganciata all'evento click lato client del pulsante Elimina.

Il problema, nel nostro esempio, è che questo pulsante viene aggiunto dinamicamente dalla GridView e non c'è un riferimento diretto ad esso. Bisogna recuperare questo controllo durante la creazione delle righe ed in particolar modo durante il binding dei dati (quando ciascuna riga è già stata creata).

L'evento ideale per il nostro lavoro è RowDataBound ed il codice che inseriamo nella pagina ModificaDati.aspx è il seguente:

    Protected Sub gvCustomers_RowDataBound(ByVal sender As Object, _
                                        ByVal e As System.Web.UI.WebControls.GridViewRowEventArgs) _
                                        Handles gvCustomers.RowDataBound
        If e.Row.RowType = DataControlRowType.DataRow Then
            Dim ButtonCtrl As WebControl = TryCast(e.Row.Cells(0).Controls(2), WebControl)
            If ButtonCtrl Is Nothing Then Return

            Dim Item As Customer = TryCast(e.Row.DataItem, Customer)

            ButtonCtrl.Attributes.Add("onclick", _
                         String.Format("javascript:return confirm('Cancellare il cliente {0}?');", _
                                       Item.CompanyName.Replace("'", "\'")))
        End If
    End Sub

Il codice è abbastanza semplice. Durante il binding delle righe di dati (RowType = DataRow) viene recuperato il controllo Link come WebControl (questo perché la GridView non utilizza il controllo LinkButton, come ci si aspetterebbe, ma un tipo di controllo DataControlLinkButton, che estende LinkButton ma che è marcato come Friend (internal in c#) e quindi inutilizzabile dall'esterno). Riferendoci ad esso come WebControl tutto funziona perfettamente anche nel caso in cui decidessimo di utilizzare un classico Button anziché un link.

Le ultime due istruzioni aggiungono la funzione javascript all'evento onclick del link visualizzando un messaggio personalizzato in base al cliente corrente. Quest'ultimo viene recuperato attraverso la proprietà DataItem dell'oggetto Row, esposto dal parametro e, che contiene l'oggetto recuperato dalla fonte dati che nel nostro caso è un tipo Customer.

Il risultato di tutto questo è il seguente:

Il postback della pagina verrà eseguito solo premendo il tasto OK.

Conclusioni
In questo articolo abbiamo visto come in ASP.NET 2.0 sia diventato più semplice gestire attività tipiche per la manipolazione dei dati scrivendo pochissimo codice.
Nei successivi articoli vedremo più da vicino come utilizzare i nuovi controlli DetailsView e FormView per gestire le operazioni di modifica dei dati.
Tutto il codice relativo a questo esempio è scaricabile dall'area download.
In merito a questo articolo, potete scrivere agli autori Antonio Catucci e Mario De Ghetto.