Aggiornamento di origini dati con DataAdapter

Il metodo Update di DataAdapter viene chiamato per applicare le modifiche apportate a un oggetto DataSet nell'origine dati. Il metodo Update, analogamente al metodo Fill, accetta come argomenti un'istanza di un oggetto DataSet e un oggetto DataTable o nome di DataTable facoltativi. L'istanza di DataSet rappresenta l'oggetto DataSet contenente le modifiche che sono state apportate e l'oggetto DataTable identifica la tabella da cui recuperare le modifiche. Se non viene specificato nessun oggetto DataTable, verrà usato il primo oggetto DataTable di DataSet.

Quando si chiama il metodo Update, DataAdapter analizza le modifiche apportate ed esegue il comando appropriato (INSERT, UPDATE o DELETE). Quando rileva una modifica a un oggetto DataAdapter, DataRow elabora la modifica usando InsertCommand, UpdateCommand o DeleteCommand. In questo modo è possibile ottimizzare le prestazioni dell'applicazione ADO.NET specificando la sintassi del comando in fase di progettazione e, dove possibile, mediante stored procedure. È necessario impostare in modo esplicito i comandi prima di chiamare Update. Se Update viene chiamato e non esiste il comando appropriato per un determinato aggiornamento, ad esempio nessun DeleteCommand per le righe cancellate, viene generata un'eccezione.

Nota

Se si usano stored procedure SQL Server per modificare o eliminare dati tramite DataAdapter, assicurarsi di non usare SET NOCOUNT ON nella definizione della stored procedure. Con tale comando il totale restituito delle righe interessate è pari a zero e tale situazione viene interpretata da DataAdapter come un conflitto di concorrenza. In questo caso verrà generata un'eccezione DBConcurrencyException.

È possibile usare i parametri Command per specificare i valori di input e di output di un'istruzione SQL o una stored procedure per ogni riga modificata in un DataSet. Per altre informazioni, vedere Parametri di DataAdapter.

Nota

È importante capire la differenza tra eliminazione di una riga in un oggetto DataTable e rimozione della riga. Quando si chiama il metodo Remove o RemoveAt, la riga viene rimossa immediatamente. Eventuali righe corrispondenti nell'origine dati di back-end non saranno interessate se si passa DataTable o DataSet a un oggetto DataAdapter e si chiama Update. Quando si usa il metodo Delete, la riga rimane in DataTable e viene contrassegnato per l'eliminazione. Se quindi si passa DataTable o DataSet a un oggetto DataAdapter e si chiama Update, la riga corrispondente nell'origine dati di back-end verrà eliminata.

Se DataTable esegue il mapping o viene generato da una singola tabella di database, è possibile usare l'oggetto DbCommandBuilder per generare automaticamente gli oggetti DeleteCommand, InsertCommand e UpdateCommand di DataAdapter. Per altre informazioni, vedere Generazione dei comandi con CommandBuilders.

Uso di UpdatedRowSource per eseguire il mapping di valori su un oggetto DataSet

Per controllare come viene eseguito il mapping dei valori restituiti dall'origine dati su un oggetto DataTable in seguito a una chiamata al metodo Update di un oggetto DataAdapter, è possibile usare la proprietà UpdatedRowSource dell'oggetto DbCommand. Impostando la proprietà UpdatedRowSource su uno dei valori di enumerazione UpdateRowSource, è possibile controllare se i parametri restituiti dai comandi di DataAdapter vengono ignorati o applicati alla riga modificata in DataSet. È possibile inoltre specificare se la prima riga restituita (se esiste) viene applicata alla riga modificata in DataTable.

La tabella seguente descrive i diversi valori dell'enumerazione UpdateRowSource e viene illustrato il modo in cui influenzano il comportamento di un comando usato con un DataAdapter.

Enumerazione UpdatedRowSource Descrizione
Both È possibile eseguire il mapping dei parametri di output e della prima riga di un set di risultati restituiti sulla riga modificata nel DataSet.
FirstReturnedRecord È possibile eseguire il mapping sulla riga modificata nel DataSet solo dei dati nella prima riga di un set di risultati restituiti.
None Tutti i parametri di output, o righe, di un set di risultati restituiti vengono ignorati.
OutputParameters È possibile eseguire il mapping sulla riga modificata nel DataSet solo dei parametri di output.

Il metodo Update applica le modifiche nell'origine dati, ma è possibile che altri client abbiano modificato i dati nell'origine dati dall'ultima volta che è stato compilato il DataSet. Per aggiornare DataSet con i dati correnti, usare i metodi DataAdapter e Fill. Le nuove righe verranno aggiunte alla tabella e le informazioni aggiornate verranno inserite nelle righe esistenti. Quando si esegue il metodo Fill, verrà determinato se aggiungere una nuova riga o aggiornare una riga esistente esaminando i valori delle chiavi primarie delle righe del DataSet e delle righe restituite da SelectCommand. Se il metodo Fill rileva un valore di chiave primaria di una riga del DataSet che corrisponde al valore di chiave primaria di una riga presente nei risultati restituiti da SelectCommand, la riga esistente verrà aggiornata con le informazioni presenti nella riga restituita da SelectCommand e la proprietà RowState della riga esistente verrà impostata su Unchanged. Se una riga restituita da SelectCommand presenta un valore di chiave primaria che non corrisponde ad alcuno dei valori di chiave primaria delle righe del DataSet, il metodo Fill aggiungerà una nuova riga con il valore relativo a RowState impostato su Unchanged.

Nota

Se SelectCommand restituisce i risultati di un OUTER JOIN, il DataAdapter non imposterà un valore PrimaryKey per l'oggetto DataTable risultante. Per assicurarsi che le righe duplicate vengano risolte correttamente, sarà necessario definire PrimaryKey in modo autonomo. Per altre informazioni, vedere Definizione delle chiavi primarie.

Per gestire le eccezioni che possono verificarsi quando si chiama il metodo Update, è possibile usare l'evento RowUpdated per rispondere agli errori di aggiornamento delle righe nel momento in cui si verificano (vedere Gestione di eventi DataAdapter), oppure impostare DataAdapter.ContinueUpdateOnError su true prima di chiamare Update e rispondere alle informazioni sugli errori archiviate nella proprietà RowError di una determinata riga al termine dell'aggiornamento (vedere Informazioni sull’errore di riga).

Nota

Se si chiama AcceptChanges sull'oggetto DataSet, DataTable o DataRow, tutti i valori Original relativi a un oggetto DataRow verranno sovrascritti con i valori Current relativi a DataRow. Se i valori di campo che identificano la riga come univoca sono stati modificati, dopo la chiamata a AcceptChanges i valori Original non corrisponderanno più ai valori dell'origine dati. AcceptChanges viene chiamato automaticamente per ogni riga durante una chiamata al metodo Update di un oggetto DataAdapter. Per mantenere i valori originali durante una chiamata al metodo Update, impostare prima la proprietà AcceptChangesDuringUpdate di DataAdapter su false oppure creare un gestore eventi per l'evento RowUpdated e impostare Status su SkipCurrentRow. Per altre informazioni, vedere Unione dei contenuti DataSet e Gestione degli eventi DataAdapter.

Esempio

Gli esempi seguenti illustrano come aggiornare le righe modificate impostando in modo esplicito UpdateCommand di DataAdapter e chiamando il relativo metodo Update. Notare che il parametro specificato nella clausola WHERE dell'istruzione UPDATE viene impostato in modo da usare il valore Original di SourceColumn. Questo è importante in quanto è possibile che il valore Current sia stato modificato e che non corrisponda più al valore nell'origine dati. Il valore Original è il valore che è stato usato per compilare DataTable dall'origine dati.

static void AdapterUpdate(string connectionString)
{
    using (SqlConnection connection =
               new(connectionString))
    {
        SqlDataAdapter dataAdapter = new(
          "SELECT CategoryID, CategoryName FROM Categories",
          connection)
        {
            UpdateCommand = new SqlCommand(
           "UPDATE Categories SET CategoryName = @CategoryName " +
           "WHERE CategoryID = @CategoryID", connection)
        };

        dataAdapter.UpdateCommand.Parameters.Add(
           "@CategoryName", SqlDbType.NVarChar, 15, "CategoryName");

        SqlParameter parameter = dataAdapter.UpdateCommand.Parameters.Add(
          "@CategoryID", SqlDbType.Int);
        parameter.SourceColumn = "CategoryID";
        parameter.SourceVersion = DataRowVersion.Original;

        DataTable categoryTable = new();
        dataAdapter.Fill(categoryTable);

        DataRow categoryRow = categoryTable.Rows[0];
        categoryRow["CategoryName"] = "New Beverages";

        dataAdapter.Update(categoryTable);

        Console.WriteLine("Rows after update.");
        foreach (DataRow row in categoryTable.Rows)
        {
            {
                Console.WriteLine("{0}: {1}", row[0], row[1]);
            }
        }
    }
}
Private Sub AdapterUpdate(ByVal connectionString As String)

    Using connection As SqlConnection = New SqlConnection( _
       connectionString)

        Dim adapter As SqlDataAdapter = New SqlDataAdapter( _
          "SELECT CategoryID, CategoryName FROM dbo.Categories", _
          connection)

        adapter.UpdateCommand = New SqlCommand( _
          "UPDATE Categories SET CategoryName = @CategoryName " & _
           "WHERE CategoryID = @CategoryID", connection)

        adapter.UpdateCommand.Parameters.Add( _
           "@CategoryName", SqlDbType.NVarChar, 15, "CategoryName")

        Dim parameter As SqlParameter = _
           adapter.UpdateCommand.Parameters.Add( _
           "@CategoryID", SqlDbType.Int)
        parameter.SourceColumn = "CategoryID"
        parameter.SourceVersion = DataRowVersion.Original

        Dim categoryTable As New DataTable
        adapter.Fill(categoryTable)

        Dim categoryRow As DataRow = categoryTable.Rows(0)
        categoryRow("CategoryName") = "New Beverages"

        adapter.Update(categoryTable)

        Console.WriteLine("Rows after update.")
        Dim row As DataRow
        For Each row In categoryTable.Rows
            Console.WriteLine("{0}: {1}", row(0), row(1))
        Next
    End Using
End Sub

Colonne AutoIncrement

Se nelle tabelle dell'origine dati sono presenti colonne con incremento automatico, è possibile riempire le colonne nel DataSet restituendo il valore dell'incremento automatico come un parametro di output di una stored procedure ed eseguendone il mapping su una colonna della tabella, restituendo il valore dell'incremento automatico nella prima riga di un set di risultati restituito da una stored procedure o un'istruzione SQL oppure usando l'evento RowUpdated di DataAdapter per eseguire un'ulteriore istruzione SELECT. Per altre informazioni e un esempio, vedere Recupero di valori di identità o di numerazione automatica.

Ordine di Insert, Update e Delete

In molti casi l'ordine in cui le modifiche apportate mediante DataSet vengono inviate all'origine dati è importante. Se ad esempio il valore di una chiave primaria relativo a una riga esistente viene aggiornato ed è stata aggiunta una nuova riga con il nuovo valore della chiave primaria come chiave esterna, è importante elaborare l'aggiornamento prima di effettuare l'inserimento.

È possibile usare il metodo Select di DataTable per restituire una matrice DataRow che faccia riferimento solo a righe con un particolare RowState. È quindi possibile passare la matrice DataRow restituita al metodo Update di DataAdapter per elaborare le righe modificate. Se si specifica un subset di righe da aggiornare, è possibile controllare l'ordine in cui vengono elaborati gli inserimenti, gli aggiornamenti e le eliminazioni.

Il codice seguente, ad esempio, assicura che vengano elaborate prima le righe cancellate della tabella, quindi le righe aggiornate e infine le righe inserite.

Dim table As DataTable = dataSet.Tables("Customers")

' First process deletes.
dataSet.Update(table.Select(Nothing, Nothing, _
  DataViewRowState.Deleted))

' Next process updates.
adapter.Update(table.Select(Nothing, Nothing, _
  DataViewRowState.ModifiedCurrent))

' Finally, process inserts.
adapter.Update(table.Select(Nothing, Nothing, _
  DataViewRowState.Added))
DataTable table = dataSet.Tables["Customers"];

// First process deletes.
adapter.Update(table.Select(null, null, DataViewRowState.Deleted));

// Next process updates.
adapter.Update(table.Select(null, null,
  DataViewRowState.ModifiedCurrent));

// Finally, process inserts.
adapter.Update(table.Select(null, null, DataViewRowState.Added));

Uso di un oggetto DataAdapter per recuperare e aggiornare dati

È possibile usare un oggetto DataAdapter per recuperare e aggiornare i dati.

  • Nell'esempio viene usato DataAdapter.AcceptChangesDuringFill per clonare i dati nel database. Se la proprietà è impostata su false, AcceptChanges non viene chiamato quando si compila la tabella e le righe appena aggiunte vengono considerate come righe inserite. Di conseguenza, nell'esempio queste righe vengono usate per inserire nuove righe nel database.

  • Negli esempi viene usato DataAdapter.TableMappings per definire il mapping tra la tabella di origine e DataTable.

  • Nell'esempio viene usato DataAdapter.FillLoadOption per determinare come l'adattatore riempie l'elemento DataTable dall'elemento DbDataReader. Quando si crea un oggetto DataTable, è possibile scrivere i dati solo dal database alla versione corrente o alla versione originale impostando la proprietà su LoadOption.Upsert o LoadOption.PreserveChanges.

  • Viene inoltre aggiornata la tabella usando DbDataAdapter.UpdateBatchSize per eseguire operazioni batch.

Prima di compilare ed eseguire l'esempio, è necessario creare il database di esempio:

USE [master]
GO

CREATE DATABASE [MySchool]

GO

USE [MySchool]
GO

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Course]([CourseID] [nvarchar](10) NOT NULL,
[Year] [smallint] NOT NULL,
[Title] [nvarchar](100) NOT NULL,
[Credits] [int] NOT NULL,
[DepartmentID] [int] NOT NULL,
 CONSTRAINT [PK_Course] PRIMARY KEY CLUSTERED
(
[CourseID] ASC,
[Year] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]) ON [PRIMARY]

GO

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Department]([DepartmentID] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](50) NOT NULL,
[Budget] [money] NOT NULL,
[StartDate] [datetime] NOT NULL,
[Administrator] [int] NULL,
 CONSTRAINT [PK_Department] PRIMARY KEY CLUSTERED
(
[DepartmentID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]) ON [PRIMARY]

GO

INSERT [dbo].[Course] ([CourseID], [Year], [Title], [Credits], [DepartmentID]) VALUES (N'C1045', 2012, N'Calculus', 4, 7)
INSERT [dbo].[Course] ([CourseID], [Year], [Title], [Credits], [DepartmentID]) VALUES (N'C1061', 2012, N'Physics', 4, 1)
INSERT [dbo].[Course] ([CourseID], [Year], [Title], [Credits], [DepartmentID]) VALUES (N'C2021', 2012, N'Composition', 3, 2)
INSERT [dbo].[Course] ([CourseID], [Year], [Title], [Credits], [DepartmentID]) VALUES (N'C2042', 2012, N'Literature', 4, 2)

SET IDENTITY_INSERT [dbo].[Department] ON

INSERT [dbo].[Department] ([DepartmentID], [Name], [Budget], [StartDate], [Administrator]) VALUES (1, N'Engineering', 350000.0000, CAST(0x0000999C00000000 AS DateTime), 2)
INSERT [dbo].[Department] ([DepartmentID], [Name], [Budget], [StartDate], [Administrator]) VALUES (2, N'English', 120000.0000, CAST(0x0000999C00000000 AS DateTime), 6)
INSERT [dbo].[Department] ([DepartmentID], [Name], [Budget], [StartDate], [Administrator]) VALUES (4, N'Economics', 200000.0000, CAST(0x0000999C00000000 AS DateTime), 4)
INSERT [dbo].[Department] ([DepartmentID], [Name], [Budget], [StartDate], [Administrator]) VALUES (7, N'Mathematics', 250024.0000, CAST(0x0000999C00000000 AS DateTime), 3)
SET IDENTITY_INSERT [dbo].[Department] OFF

ALTER TABLE [dbo].[Course]  WITH CHECK ADD  CONSTRAINT [FK_Course_Department] FOREIGN KEY([DepartmentID])
REFERENCES [dbo].[Department] ([DepartmentID])
GO
ALTER TABLE [dbo].[Course] CHECK CONSTRAINT [FK_Course_Department]
GO
using System;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Linq;
using CSDataAdapterOperations.Properties;

namespace CSDataAdapterOperations.Properties {
   internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {

      private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));

      public static Settings Default {
         get {
            return defaultInstance;
         }
      }

      [global::System.Configuration.ApplicationScopedSettingAttribute()]
      [global::System.Configuration.DefaultSettingValueAttribute("Data Source=(local);Initial Catalog=MySchool;Integrated Security=True")]
      public string MySchoolConnectionString {
         get {
            return ((string)(this["MySchoolConnectionString"]));
         }
      }
   }
}

class Program {
   static void Main(string[] args) {
      Settings settings = new Settings();

      // Copy the data from the database.  Get the table Department and Course from the database.
      String selectString = @"SELECT [DepartmentID],[Name],[Budget],[StartDate],[Administrator]
                                     FROM [MySchool].[dbo].[Department];

                                   SELECT [CourseID],@Year as [Year],Max([Title]) as [Title],
                                   Max([Credits]) as [Credits],Max([DepartmentID]) as [DepartmentID]
                                   FROM [MySchool].[dbo].[Course]
                                   Group by [CourseID]";

      DataSet mySchool = new DataSet();

      SqlCommand selectCommand = new SqlCommand(selectString);
      SqlParameter parameter = selectCommand.Parameters.Add("@Year", SqlDbType.SmallInt, 2);
      parameter.Value = new Random(DateTime.Now.Millisecond).Next(9999);

      // Use DataTableMapping to map the source tables and the destination tables.
      DataTableMapping[] tableMappings = {new DataTableMapping("Table", "Department"), new DataTableMapping("Table1", "Course")};
      CopyData(mySchool, settings.MySchoolConnectionString, selectCommand, tableMappings);

      Console.WriteLine("The following tables are from the database.");
      foreach (DataTable table in mySchool.Tables) {
         Console.WriteLine(table.TableName);
         ShowDataTable(table);
      }

      // Roll back the changes
      DataTable department = mySchool.Tables["Department"];
      DataTable course = mySchool.Tables["Course"];

      department.Rows[0]["Name"] = "New" + department.Rows[0][1];
      course.Rows[0]["Title"] = "New" + course.Rows[0]["Title"];
      course.Rows[0]["Credits"] = 10;

      Console.WriteLine("After we changed the tables:");
      foreach (DataTable table in mySchool.Tables) {
         Console.WriteLine(table.TableName);
         ShowDataTable(table);
      }

      department.RejectChanges();
      Console.WriteLine("After use the RejectChanges method in Department table to roll back the changes:");
      ShowDataTable(department);

      DataColumn[] primaryColumns = { course.Columns["CourseID"] };
      DataColumn[] resetColumns = { course.Columns["Title"] };
      ResetCourse(course, settings.MySchoolConnectionString, primaryColumns, resetColumns);
      Console.WriteLine("After use the ResetCourse method in Course table to roll back the changes:");
      ShowDataTable(course);

      // Batch update the table.
      String insertString = @"Insert into [MySchool].[dbo].[Course]([CourseID],[Year],[Title],
                                   [Credits],[DepartmentID])
             values (@CourseID,@Year,@Title,@Credits,@DepartmentID)";
      SqlCommand insertCommand = new SqlCommand(insertString);
      insertCommand.Parameters.Add("@CourseID", SqlDbType.NVarChar, 10, "CourseID");
      insertCommand.Parameters.Add("@Year", SqlDbType.SmallInt, 2, "Year");
      insertCommand.Parameters.Add("@Title", SqlDbType.NVarChar, 100, "Title");
      insertCommand.Parameters.Add("@Credits", SqlDbType.Int, 4, "Credits");
      insertCommand.Parameters.Add("@DepartmentID", SqlDbType.Int, 4, "DepartmentID");

      const Int32 batchSize = 10;
      BatchInsertUpdate(course, settings.MySchoolConnectionString, insertCommand, batchSize);
   }

   private static void CopyData(DataSet dataSet, String connectionString, SqlCommand selectCommand, DataTableMapping[] tableMappings) {
      using (SqlConnection connection = new SqlConnection(connectionString)) {
         selectCommand.Connection = connection;

         connection.Open();

         using (SqlDataAdapter adapter = new SqlDataAdapter(selectCommand)) {adapter.TableMappings.AddRange(tableMappings);
            // If set the AcceptChangesDuringFill as the false, AcceptChanges will not be called on a
            // DataRow after it is added to the DataTable during any of the Fill operations.
            adapter.AcceptChangesDuringFill = false;

            adapter.Fill(dataSet);
         }
      }
   }

   // Roll back only one column or several columns data of the Course table by call ResetDataTable method.
   private static void ResetCourse(DataTable table, String connectionString,
       DataColumn[] primaryColumns, DataColumn[] resetColumns) {
      table.PrimaryKey = primaryColumns;

      // Build the query string
      String primaryCols = String.Join(",", primaryColumns.Select(col => col.ColumnName));
      String resetCols = String.Join(",", resetColumns.Select(col => $"Max({col.ColumnName}) as {col.ColumnName}"));

      String selectString = $"Select {primaryCols},{resetCols} from Course Group by {primaryCols}");

      SqlCommand selectCommand = new SqlCommand(selectString);

      ResetDataTable(table, connectionString, selectCommand);
   }

   // RejectChanges will roll back all changes made to the table since it was loaded, or the last time AcceptChanges
   // was called. When you copy from the database, you can lose all the data after calling RejectChanges
   // The ResetDataTable method rolls back one or more columns of data.
   private static void ResetDataTable(DataTable table, String connectionString,
       SqlCommand selectCommand) {
      using (SqlConnection connection = new SqlConnection(connectionString)) {
         selectCommand.Connection = connection;

         connection.Open();

         using (SqlDataAdapter adapter = new SqlDataAdapter(selectCommand)) {
            // The incoming values for this row will be written to the current version of each
            // column. The original version of each column's data will not be changed.
            adapter.FillLoadOption = LoadOption.Upsert;

            adapter.Fill(table);
         }
      }
   }

   private static void BatchInsertUpdate(DataTable table, String connectionString,
       SqlCommand insertCommand, Int32 batchSize) {
      using (SqlConnection connection = new SqlConnection(connectionString)) {
         insertCommand.Connection = connection;
         // When setting UpdateBatchSize to a value other than 1, all the commands
         // associated with the SqlDataAdapter have to have their UpdatedRowSource
         // property set to None or OutputParameters. An exception is thrown otherwise.
         insertCommand.UpdatedRowSource = UpdateRowSource.None;

         connection.Open();

         using (SqlDataAdapter adapter = new SqlDataAdapter()) {
            adapter.InsertCommand = insertCommand;
            // Gets or sets the number of rows that are processed in each round-trip to the server.
            // Setting it to 1 disables batch updates, as rows are sent one at a time.
            adapter.UpdateBatchSize = batchSize;

            adapter.Update(table);

            Console.WriteLine("Successfully to update the table.");
         }
      }
   }

   private static void ShowDataTable(DataTable table) {
      foreach (DataColumn col in table.Columns) {
         Console.Write("{0,-14}", col.ColumnName);
      }
      Console.WriteLine("{0,-14}", "RowState");

      foreach (DataRow row in table.Rows) {
         foreach (DataColumn col in table.Columns) {
            if (col.DataType.Equals(typeof(DateTime)))
               Console.Write("{0,-14:d}", row[col]);
            else if (col.DataType.Equals(typeof(Decimal)))
               Console.Write("{0,-14:C}", row[col]);
            else
               Console.Write("{0,-14}", row[col]);
         }
         Console.WriteLine("{0,-14}", row.RowState);
      }
   }
}

Vedi anche