Crear una capa de lógica empresarial (VB)

por Scott Mitchell

Descargar PDF

En este tutorial veremos cómo centralizar las reglas de negocio en una capa de lógica empresarial (BLL) que actúa como intermediaria para el intercambio de datos entre la capa de presentación y la DAL.

Introducción

La capa de acceso a datos (DAL) creada en el primer tutorial distingue claramente la lógica de acceso a datos de la lógica de presentación. Sin embargo, aunque la DAL distingue claramente entre los detalles de acceso a datos y la capa de presentación, no exige el cumplimiento de ninguna regla empresarial que pueda ser de aplicación. Por ejemplo, en nuestra aplicación, puede que no queramos permitir que los campos CategoryID o SupplierID de la tabla Products se modifiquen cuando el campo Discontinued esté establecido en 1, o bien queramos aplicar reglas de antigüedad que prohíban situaciones en las que un empleado rinda cuentas ante alguien contratado después de él. Otro escenario común es la autorización, por ejemplo, que solo los usuarios de un rol determinado puedan eliminar productos o cambiar el valor de UnitPrice.

En este tutorial veremos cómo centralizar las reglas empresariales en una capa de lógica empresarial (BLL) que actúa como intermediaria para el intercambio de datos entre la capa de presentación y la DAL. En una aplicación del mundo real, la BLL debe implementarse como un proyecto de biblioteca de clases independiente; sin embargo, en estos tutoriales implementaremos la BLL como una serie de clases en la carpeta App_Code para simplificar la estructura del proyecto. En la figura 1 se muestran las relaciones arquitectónicas entre la capa de presentación, la BLL y la DAL.

The BLL Separates the Presentation Layer from the Data Access Layer and Imposes Business Rules

Figura 1: La BLL separa la capa de presentación de la capa de acceso a datos e impone reglas empresariales

En lugar de crear clases independientes para implementar nuestra lógica de negocio, podemos colocar esta lógica directamente en el conjunto de datos tipado con clases parciales. Para obtener un ejemplo de creación y extensión de un conjunto de datos tipado, consulte el primer tutorial.

Paso 1: Crear las clases de la BLL

Nuestra BLL se compone de cuatro clases (una para cada elemento TableAdapter de la DAL). Cada una de estas clases de BLL tendrá métodos para recuperar, insertar, actualizar y eliminar en el elemento TableAdapter respectivo de la DAL, aplicando para ello las reglas empresariales adecuadas.

Para distinguir más claramente las clases relativas a la DAL y la BLL, vamos a crear dos subcarpetas en la carpeta App_Code: DAL y BLL. Basta con hacer clic con el botón derecho en la carpeta App_Code del Explorador de soluciones y seleccionar Nueva carpeta. Después de crear estas dos carpetas, mueva el objeto DataSet con tipo que creamos en el primer tutorial a la subcarpeta DAL.

A continuación, cree los cuatro archivos de clase BLL en la subcarpeta BLL. Para ello, haga clic con el botón derecho en la subcarpeta BLL, seleccione Agregar un nuevo elemento y, luego, la plantilla Clase. Denomine las cuatro clases ProductsBLL, CategoriesBLL, SuppliersBLL y EmployeesBLL.

Add Four New Classes to the App_Code Folder

Figura 2: Adición de cuatro clases nuevas a la carpeta App_Code

Ahora, vamos a agregar métodos a cada una de las clases para, simplemente, ajustar los métodos definidos para elementos TableAdapter del primer tutorial. Por ahora, estos métodos solo llamarán directamente a la DAL; volveremos más adelante para agregar cualquier lógica empresarial necesaria.

Nota:

Si usa Visual Studio Standard Edition o superior (es decir, no usa Visual Web Developer), puede diseñar las clases visualmente si lo desea mediante el Diseñador de clases. Consulte este blog sobre el Diseñador de clases para obtener más información sobre esta nueva característica de Visual Studio.

En la clase ProductsBLL es necesario agregar un total de siete métodos:

  • GetProducts() devuelve todos los productos.
  • GetProductByProductID(productID) obtiene el producto con el identificador de producto especificado.
  • GetProductsByCategoryID(categoryID) devuelve todos los productos de la categoría especificada.
  • GetProductsBySupplier(supplierID) devuelve todos los productos del proveedor especificado.
  • AddProduct(productName, supplierID, categoryID, quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder, reorderLevel, discontinued) inserta un nuevo producto en la base de datos usando los valores pasados; devuelve el valor de ProductID del registro recién insertado.
  • UpdateProduct(productName, supplierID, categoryID, quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder, reorderLevel, discontinued, productID) actualiza un producto existente en la base de datos usando los valores pasados; devuelve True si se ha actualizado justamente una fila; de lo contrario, devuelve False.
  • DeleteProduct(productID) elimina el producto especificado de la base de datos.

ProductsBLL.vb

Imports NorthwindTableAdapters

<System.ComponentModel.DataObject()> _
Public Class ProductsBLL

    Private _productsAdapter As ProductsTableAdapter = Nothing
    Protected ReadOnly Property Adapter() As ProductsTableAdapter
        Get
            If _productsAdapter Is Nothing Then
                _productsAdapter = New ProductsTableAdapter()
            End If

            Return _productsAdapter
        End Get
    End Property

    <System.ComponentModel.DataObjectMethodAttribute _
        (System.ComponentModel.DataObjectMethodType.Select, True)> _
    Public Function GetProducts() As Northwind.ProductsDataTable
        Return Adapter.GetProducts()
    End Function

    <System.ComponentModel.DataObjectMethodAttribute _
        (System.ComponentModel.DataObjectMethodType.Select, False)> _
    Public Function GetProductByProductID(ByVal productID As Integer) _
        As Northwind.ProductsDataTable
        Return Adapter.GetProductByProductID(productID)
    End Function

    <System.ComponentModel.DataObjectMethodAttribute _
        (System.ComponentModel.DataObjectMethodType.Select, False)> _
    Public Function GetProductsByCategoryID(ByVal categoryID As Integer) _
        As Northwind.ProductsDataTable
        Return Adapter.GetProductsByCategoryID(categoryID)
    End Function

    <System.ComponentModel.DataObjectMethodAttribute _
        (System.ComponentModel.DataObjectMethodType.Select, False)> _
    Public Function GetProductsBySupplierID(ByVal supplierID As Integer) _
        As Northwind.ProductsDataTable
        Return Adapter.GetProductsBySupplierID(supplierID)
    End Function

    <System.ComponentModel.DataObjectMethodAttribute _
        (System.ComponentModel.DataObjectMethodType.Insert, True)> _
    Public Function AddProduct( _
        productName As String, supplierID As Nullable(Of Integer), _
        categoryID As Nullable(Of Integer), quantityPerUnit As String, _
        unitPrice As Nullable(Of Decimal), unitsInStock As Nullable(Of Short), _
        unitsOnOrder As Nullable(Of Short), reorderLevel As Nullable(Of Short), _
        discontinued As Boolean) _
        As Boolean

        Dim products As New Northwind.ProductsDataTable()
        Dim product As Northwind.ProductsRow = products.NewProductsRow()

        product.ProductName = productName
        If Not supplierID.HasValue Then
            product.SetSupplierIDNull()
        Else
            product.SupplierID = supplierID.Value
        End If

        If Not categoryID.HasValue Then
            product.SetCategoryIDNull()
        Else
            product.CategoryID = categoryID.Value
        End If

        If quantityPerUnit Is Nothing Then
            product.SetQuantityPerUnitNull()
        Else
            product.QuantityPerUnit = quantityPerUnit
        End If

        If Not unitPrice.HasValue Then
            product.SetUnitPriceNull()
        Else
            product.UnitPrice = unitPrice.Value
        End If

        If Not unitsInStock.HasValue Then
            product.SetUnitsInStockNull()
        Else
            product.UnitsInStock = unitsInStock.Value
        End If

        If Not unitsOnOrder.HasValue Then
            product.SetUnitsOnOrderNull()
        Else
            product.UnitsOnOrder = unitsOnOrder.Value
        End If

        If Not reorderLevel.HasValue Then
            product.SetReorderLevelNull()
        Else
            product.ReorderLevel = reorderLevel.Value
        End If

        product.Discontinued = discontinued

        products.AddProductsRow(product)
        Dim rowsAffected As Integer = Adapter.Update(products)

        Return rowsAffected = 1
    End Function

    <System.ComponentModel.DataObjectMethodAttribute _
        (System.ComponentModel.DataObjectMethodType.Update, True)> _
    Public Function UpdateProduct(_
        productName As String, supplierID As Nullable(Of Integer), _
        categoryID As Nullable(Of Integer), quantityPerUnit As String, _
        unitPrice As Nullable(Of Decimal), unitsInStock As Nullable(Of Short), _
        unitsOnOrder As Nullable(Of Short), reorderLevel As Nullable(Of Short), _
        discontinued As Boolean, productID As Integer) _
        As Boolean

        Dim products As Northwind.ProductsDataTable = _
            Adapter.GetProductByProductID(productID)

        If products.Count = 0 Then
            Return False
        End If

        Dim product as Northwind.ProductsRow = products(0)

        product.ProductName = productName
        If Not supplierID.HasValue Then
            product.SetSupplierIDNull()
        Else
            product.SupplierID = supplierID.Value
        End If

        If Not categoryID.HasValue Then
            product.SetCategoryIDNull()
        Else
            product.CategoryID = categoryID.Value
        End If

        If quantityPerUnit Is Nothing Then
            product.SetQuantityPerUnitNull()
        Else
            product.QuantityPerUnit = quantityPerUnit
        End If

        If Not unitPrice.HasValue Then
            product.SetUnitPriceNull()
        Else
            product.UnitPrice = unitPrice.Value
        End If

        If Not unitsInStock.HasValue Then
            product.SetUnitsInStockNull()
        Else
            product.UnitsInStock = unitsInStock.Value
        End If

        If Not unitsOnOrder.HasValue Then
            product.SetUnitsOnOrderNull()
        Else
            product.UnitsOnOrder = unitsOnOrder.Value
        End If

        If Not reorderLevel.HasValue Then
            product.SetReorderLevelNull()
        Else
            product.ReorderLevel = reorderLevel.Value
        End If

        product.Discontinued = discontinued

        Dim rowsAffected As Integer = Adapter.Update(product)

        Return rowsAffected = 1
    End Function

    <System.ComponentModel.DataObjectMethodAttribute _
        (System.ComponentModel.DataObjectMethodType.Delete, True)> _
    Public Function DeleteProduct(ByVal productID As Integer) As Boolean
        Dim rowsAffected As Integer = Adapter.Delete(productID)

        Return rowsAffected = 1
    End Function
End Class

Los métodos que simplemente devuelven los datos GetProducts, GetProductByProductID, GetProductsByCategoryID y GetProductBySuppliersID son bastante sencillos, ya que simplemente llaman a la DAL. Aunque en algunos escenarios puede haber reglas empresariales que deban implementarse en este nivel (como reglas de autorización basadas en el usuario que ha iniciado sesión actualmente o el rol al que pertenece el usuario), simplemente dejaremos estos métodos tal cual. En estos métodos, la BLL actúa meramente como un proxy a través del cual la capa de presentación accede a los datos subyacentes desde la capa de acceso a datos.

Los métodos AddProduct y UpdateProduct toman como parámetros los valores de los distintos campos de producto y agregan un nuevo producto o actualizan uno existente, respectivamente. Dado que muchas de las columnas de la tabla Product pueden aceptar valores NULL (CategoryID, SupplierID y UnitPrice, por decir algunos), esos parámetros de entrada de AddProduct y UpdateProduct que se asignan a estas columnas usan tipos que aceptan valores NULL. Los tipos que aceptan valores NULL son nuevos en .NET 2.0, y proporcionan una técnica para indicar si un tipo de valor debe ser Nothing en su lugar. Consulte la entrada de blog de Paul VickThe Truth About Nullable Types and VB y la documentación técnica de la estructura que acepta valores NULL para obtener más información.

Los tres métodos devuelven un valor booleano que indica si se ha insertado, actualizado o eliminado una fila, ya que es posible que la operación no resulte en una fila afectada. Por ejemplo, si el desarrollador de la página llama a DeleteProduct pasando un valor de ProductID de un producto que no existe, la instrucción DELETE emitida a la base de datos no tendrá ningún efecto y, por tanto, el método DeleteProduct devolverá False.

Cabe decir que, al agregar un nuevo producto o actualizar uno existente, tomamos los valores de campo del producto nuevos o modificados como una lista de valores escalares, en lugar de aceptar una instancia de ProductsRow. Este enfoque es el elegido porque la clase ProductsRow deriva de la clase DataRow de ADO.NET, que no tiene un constructor sin parámetros predeterminado. Para crear una nueva instancia de ProductsRow, primero debemos crear una instancia de ProductsDataTable y, a continuación, invocar su método NewProductRow() (que es lo que hacemos en AddProduct). Esta laguna se hará más patente cuando pasemos a cómo insertar y actualizar productos mediante ObjectDataSource. Por decirlo brevemente, ObjectDataSource intentará crear una instancia de los parámetros de entrada. Si el método de la BLL espera una instancia de ProductsRow, ObjectDataSource intentará crear una, pero generará un error debido a la falta de un constructor sin parámetros predeterminado. Para obtener más información sobre este problema, consulte estas dos entradas de foros de ASP.NET: Actualizar ObjectDataSource con DastaSet fuertemente tipados y Problema con ObjectDataSource y DastaSet fuertemente tipados.

Tanto en AddProduct como en UpdateProduct, el código crea una instancia ProductsRow y la rellena con los valores que se acaban de pasar. Al asignar valores a DataColumn de un DataRow, se pueden producir varias comprobaciones de validación de nivel de campo. Por lo tanto, volver a colocar manualmente en un DataRow los valores pasados ayuda a garantizar la validez de los datos que se pasan al método de la BLL. Desafortunadamente, las clases DataRow fuertemente tipadas generadas por Visual Studio no usan tipos que aceptan valores NULL. En su lugar, para indicar que un determinado DataColumn de un DataRow debe corresponderse con un valor de base de datos NULL, deberemos usar el método SetColumnNameNull().

En UpdateProduct, primero cargamos en el producto que queremos actualizar mediante GetProductByProductID(productID). Aunque esto puede parecer una comunicación innecesaria con la base de datos, veremos que habrá valido la pena en futuros tutoriales donde exploraremos la simultaneidad optimista. La simultaneidad optimista es una técnica que garantiza que dos usuarios que trabajan simultáneamente en los mismos datos no sobrescriben los cambios del otro por error. Tomar el registro completo también facilita la creación de métodos de actualización en la BLL que solo modifican un subconjunto de columnas del DataRow. Cuando exploremos la clase SuppliersBLL, veremos un ejemplo de este tipo.

Por último, tenga en cuenta que la clase ProductsBLL tiene aplicado el atributo DataObject (la sintaxis [System.ComponentModel.DataObject] justo antes de la instrucción de clase, cerca del inicio del archivo) y que los métodos tienen atributos DataObjectMethodAttribute. El atributo DataObject marca la clase como un objeto apto para enlazarse a un control ObjectDataSource, mientras que DataObjectMethodAttribute indica el propósito del método. Como veremos en tutoriales futuros, el control ObjectDataSource de ASP.NET 2.0 facilita el acceso declarativo a datos desde una clase. Para ayudar a filtrar la lista de posibles clases a las que enlazar en el asistente del ObjectDataSource, la lista desplegable de dicho asistente solo mostrará de forma predeterminada esas clases marcadas como DataObjects. La clase ProductsBLL funcionará igual de bien sin estos atributos, pero agregarlos facilita el trabajo en el asistente del ObjectDataSource.

Agregar otras clases

Con la clase ProductsBLL completa, todavía queda agregar las clases para trabajar con categorías, proveedores y empleados. Dedique un momento a crear las siguientes clases y métodos mediante los conceptos del ejemplo anterior:

  • CategoriesBLL.cs

    • GetCategories()
    • GetCategoryByCategoryID(categoryID)
  • SuppliersBLL.cs

    • GetSuppliers()
    • GetSupplierBySupplierID(supplierID)
    • GetSuppliersByCountry(country)
    • UpdateSupplierAddress(supplierID, address, city, country)
  • EmployeesBLL.cs

    • GetEmployees()
    • GetEmployeeByEmployeeID(employeeID)
    • GetEmployeesByManager(managerID)

El método que merece la pena tener en cuenta es el método UpdateSupplierAddress de la clase SuppliersBLL. Este método proporciona una interfaz para actualizar solo la información de dirección del proveedor. Internamente, este método lee en el objeto SupplierDataRow en busca del supplierID especificado (mediante GetSupplierBySupplierID), establece sus propiedades de dirección y, a continuación, llama al método de Update de SupplierDataTable. Este es el método UpdateSupplierAddress:

<System.ComponentModel.DataObjectMethodAttribute _
    (System.ComponentModel.DataObjectMethodType.Update, True)> _
Public Function UpdateSupplierAddress(ByVal supplierID As Integer, _
    ByVal address As String, ByVal city As String, ByVal country As String) _
    As Boolean

    Dim suppliers As Northwind.SuppliersDataTable = _
        Adapter.GetSupplierBySupplierID(supplierID)

    If suppliers.Count = 0 Then
        Return False
    Else
        Dim supplier As Northwind.SuppliersRow = suppliers(0)

        If address Is Nothing Then
            supplier.SetAddressNull()
        Else
            supplier.Address = address
        End If

        If city Is Nothing Then
            supplier.SetCityNull()
        Else
            supplier.City = city
        End If

        If country Is Nothing Then
            supplier.SetCountryNull()
        Else
            supplier.Country = country
        End If

        Dim rowsAffected As Integer = Adapter.Update(supplier)

        Return rowsAffected = 1
    End If
End Function

Consulte la descarga de este artículo para obtener la implementación completa de las clases de BLL.

Paso 2: Acceder a los objetos DataSet con tipo a través de las clases de BLL

En el primer tutorial vimos ejemplos de cómo trabajar directamente con el objeto DataSet con tipo mediante programación, pero con la adición de las clases de BLL, el nivel de presentación debería funcionar en su lugar con la BLL. En el ejemplo de AllProducts.aspx del primer tutorial, se usó ProductsTableAdapter para enlazar la lista de productos a un GridView, como se muestra en el código siguiente:

Dim productsAdapter As New ProductsTableAdapter()
GridView1.DataSource = productsAdapter.GetProducts()
GridView1.DataBind()

Para usar las nuevas clases de BLL, lo único que debe cambiar es la primera línea de código: simplemente, reemplace el objeto ProductsTableAdapter por un objeto ProductBLL:

Dim productLogic As New ProductsBLL()
GridView1.DataSource = productLogic.GetProducts()
GridView1.DataBind()

También se puede acceder a las clases de BLL mediante declaración (al igual que el objeto DataSet con tipo) usando el ObjectDataSource. Analizaremos el ObjectDataSource con más detalle en los tutoriales siguientes.

The List of Products is Displayed in a GridView

Figura 3: La lista de productos se muestra en un GridView (haga clic para ver la imagen a tamaño completo)

Paso 3: Agregar validación de nivel de campo a las clases DataRow

La validación de nivel de campo son comprobaciones que tienen que ver con los valores de propiedad de los objetos de negocio al insertarlos o actualizarlos. Estas son algunas reglas de validación de nivel de campo de productos:

  • El campo ProductName debe tener 40 caracteres o menos de longitud.
  • El campo QuantityPerUnit debe tener 20 caracteres o menos de longitud.
  • Los campos ProductID, ProductName y Discontinued son obligatorios, pero todos los demás son opcionales.
  • Los campos UnitPrice, UnitsInStock, UnitsOnOrder y ReorderLevel deben ser mayores o iguales a cero.

Estas reglas pueden y deben expresarse en el nivel de base de datos. Los límites de caracteres de los campos ProductName y QuantityPerUnit los capturan los tipos de datos de esas columnas en la tabla Products (nvarchar(40) y nvarchar(20) respectivamente). El hecho de que los campos sean obligatorios u opcionales se expresa según si la columna de tabla de base de datos permite valores NULL. Existen cuatro restricciones de comprobación que garantizan que solo puede haber valores mayores o iguales que cero en las columnas UnitPrice, UnitsInStock, UnitsOnOrder o ReorderLevel.

Además de aplicar estas reglas en la base de datos, también deben aplicarse en el nivel del DataSet. De hecho, la longitud del campo y si un valor es obligatorio u opcional ya se capturan para cada conjunto de DataColumn del DataTable. Para ver la validación de nivel de campo existente que se proporciona automáticamente, vaya al Diseñador de DataSet, seleccione un campo de una de las DataTable y, a continuación, vaya a la ventana Propiedades. Como se muestra en la figura 4, el elemento DataColumn QuantityPerUnit de ProductsDataTable tiene una longitud máxima de 20 caracteres y permite valores NULL. Si intentamos establecer la propiedad QuantityPerUnit de ProductsDataRow en un valor de cadena de más de 20 caracteres, se producirá una excepción ArgumentException.

The DataColumn Provides Basic Field-Level Validation

Figura 4: El elemento DataColumn proporciona validación de nivel de campo básica (haga clic para ver la imagen a tamaño completo)

Desafortunadamente, en la ventana Propiedades no se pueden especificar comprobaciones de límite, por ejemplo, establecer que el valor de UnitPrice debe ser mayor o igual que cero. Para proporcionar este tipo de validación de nivel de campo, es necesario crear un controlador de eventos para el evento ColumnChanging del DataTable. Como se mencionó en el tutorial anterior, los objetos DataSet, DataTable y DataRow creados por el objeto DataSet con tipo se pueden ampliar usando clases parciales. Con esta técnica, podemos crear un controlador de eventos ColumnChanging para la clase ProductsDataTable. Empecemos creando una clase en la carpeta App_Code denominada ProductsDataTable.ColumnChanging.vb.

Add a New Class to the App_Code Folder

Figura 5: Agregar una nueva clase a la carpeta App_Code (haga clic para ver la imagen a tamaño completo)

A continuación, cree un controlador de eventos para el evento ColumnChanging que garantice que los valores de columna de UnitPrice, UnitsInStock, UnitsOnOrder y ReorderLevel sean mayores o iguales que cero (si no son NULL). Si alguna de estas columnas está fuera de rango, inicie una excepción ArgumentException.

ProductsDataTable.ColumnChanging.vb

Imports System.data

Partial Public Class Northwind
    Partial Public Class ProductsDataTable
        Public Overrides Sub BeginInit()
            AddHandler Me.ColumnChanging, AddressOf ValidateColumn
        End Sub

        Sub ValidateColumn(sender As Object, e As DataColumnChangeEventArgs)
            If e.Column.Equals(Me.UnitPriceColumn) Then
                If Not Convert.IsDBNull(e.ProposedValue) AndAlso _
                    CType(e.ProposedValue, Decimal) < 0 Then
                    Throw New ArgumentException( _
                        "UnitPrice cannot be less than zero", "UnitPrice")
                End If
            ElseIf e.Column.Equals(Me.UnitsInStockColumn) OrElse _
                e.Column.Equals(Me.UnitsOnOrderColumn) OrElse _
                e.Column.Equals(Me.ReorderLevelColumn) Then
                If Not Convert.IsDBNull(e.ProposedValue) AndAlso _
                    CType(e.ProposedValue, Short) < 0 Then
                    Throw New ArgumentException(String.Format( _
                        "{0} cannot be less than zero", e.Column.ColumnName), _
                        e.Column.ColumnName)
                End If
            End If
        End Sub
    End Class
End Class

Paso 4: Agregar reglas empresariales personalizadas a las clases de BLL

Además de la validación de nivel de campo, puede haber reglas empresariales personalizadas genéricas que tengan que ver con diferentes entidades o conceptos que no puedan expresarse en el nivel de columna, como, por ejemplo:

  • Si un producto se interrumpe, su UnitPrice no se puede actualizar.
  • El país de residencia de un empleado debe ser el mismo que el país de residencia de su jefe.
  • Un producto no se puede interrumpir si es el único producto proporcionado por el proveedor.

Las clases de BLL deben contener comprobaciones para garantizar el cumplimiento de las reglas empresariales de la aplicación. Estas comprobaciones se pueden agregar directamente a los métodos donde sean de aplicación.

Imaginemos que nuestras reglas empresariales dictan que un producto no se puede marcar como interrumpido si es el único producto de un proveedor determinado. Es decir, si el producto X es el único producto que hemos adquirido del proveedor Y, no podremos marcar X como interrumpido; sin embargo, si el proveedor Y nos ha proporcionado tres productos (A, B y C), entonces sí podremos marcar uno —o todos ellos— como interrumpidos. Sí, es una regla empresarial rara, pero las reglas empresariales y el sentido común no siempre van de la mano.

Para aplicar esta regla empresarial en el método UpdateProducts, comenzaríamos comprobando si Discontinued se ha establecido en True y, si es así, llamaríamos a GetProductsBySupplierID para determinar cuántos productos hemos adquirido del proveedor de este producto. Si solo hemos adquirido un producto de este proveedor, iniciamos una excepción ApplicationException.

<System.ComponentModel.DataObjectMethodAttribute_
    (System.ComponentModel.DataObjectMethodType.Update, True)> _
Public Function UpdateProduct( _
    productName As String, supplierID As Nullable(Of Integer), _
    categoryID As Nullable(Of Integer), quantityPerUnit As String, _
    unitPrice As Nullable(Of Decimal), unitsInStock As Nullable(Of Short), _
    unitsOnOrder As Nullable(Of Short), reorderLevel As Nullable(Of Short), _
    discontinued As Boolean, productID As Integer) _
    As Boolean

    Dim products As Northwind.ProductsDataTable = _
        Adapter.GetProductByProductID(productID)

    If products.Count = 0 Then
        Return False
    End If

    Dim product As Northwind.ProductsRow = products(0)

    If discontinued Then
        Dim productsBySupplier As Northwind.ProductsDataTable = _
            Adapter.GetProductsBySupplierID(product.SupplierID)

        If productsBySupplier.Count = 1 Then
            Throw New ApplicationException( _
                "You cannot mark a product as discontinued if it is " & _
                "the only product purchased from a supplier")
        End If
    End If

    product.ProductName = productName

    If Not supplierID.HasValue Then
        product.SetSupplierIDNull()
    Else
        product.SupplierID = supplierID.Value
    End If

    If Not categoryID.HasValue Then
        product.SetCategoryIDNull()
    Else
        product.CategoryID = categoryID.Value
    End If

    If quantityPerUnit Is Nothing Then
        product.SetQuantityPerUnitNull()
    Else
        product.QuantityPerUnit = quantityPerUnit
    End If

    If Not unitPrice.HasValue Then
        product.SetUnitPriceNull()
    Else
        product.UnitPrice = unitPrice.Value
    End If

    If Not unitsInStock.HasValue Then
        product.SetUnitsInStockNull()
    Else
        product.UnitsInStock = unitsInStock.Value
    End If

    If Not unitsOnOrder.HasValue Then
        product.SetUnitsOnOrderNull()
    Else
        product.UnitsOnOrder = unitsOnOrder.Value
    End If

    If Not reorderLevel.HasValue Then
        product.SetReorderLevelNull()
    Else
        product.ReorderLevel = reorderLevel.Value
    End If

    product.Discontinued = discontinued

    Dim rowsAffected As Integer = Adapter.Update(product)

    Return rowsAffected = 1
End Function

Responder a errores de validación en el nivel de presentación

Al llamar a la BLL desde el nivel de presentación, podemos decidir si intentar controlar las excepciones que podrían iniciarse o dejar que se propaguen hasta ASP.NET (lo que desencadenará el evento Error de HttpApplication). Para controlar una excepción al trabajar con la BLL mediante programación, podemos usar un bloque Try...Catch, como se muestra en el ejemplo siguiente:

Dim productLogic As New ProductsBLL()

Try
    productLogic.UpdateProduct("Scotts Tea", 1, 1, Nothing, _
      -14, 10, Nothing, Nothing, False, 1)
Catch ae As ArgumentException
    Response.Write("There was a problem: " & ae.Message)
End Try

Como veremos en futuros tutoriales, las excepciones propagadas desde la BLL cuando se usa un control web de datos para insertar, actualizar o eliminar datos se pueden controlar directamente con un controlador de eventos, en lugar de tener que encapsular código en bloques Try...Catch.

Resumen

Una aplicación bien diseñada se crea en capas distintas, cada una de las cuales encapsula un rol determinado. En el primer tutorial de esta serie de artículos creamos una capa de acceso a datos mediante un objeto DataSet con tipo; en este tutorial, hemos creado una capa de lógica empresarial como una serie de clases en la carpeta App_Code de la aplicación que llaman a la DAL. La BLL implementa la lógica de nivel de campo y de nivel empresarial de nuestra aplicación. Además de crear una BLL independiente, como hemos hecho en este tutorial, otra opción es ampliar los métodos del objeto TableAdapter mediante el uso de clases parciales. Sin embargo, esta técnica no nos permite invalidar los métodos existentes ni separar la DAL de la BLL tan claramente como el enfoque que hemos usado en este artículo.

Con la DAL y la BLL completadas, ya estamos listos para empezar con la capa de presentación. En el siguiente tutorial nos desviaremos un poco de los temas de acceso a datos y definiremos un diseño de página uniforme para poder usarlo en todos los tutoriales.

¡Feliz programación!

Acerca del autor

Scott Mitchell, autor de siete libros de ASP/ASP.NET y fundador de 4GuysFromRolla.com, ha estado trabajando con tecnologías web de Microsoft desde 1998. Scott trabaja como consultor independiente, entrenador y escritor. Su último libro es Sams Teach Yourself ASP.NET 2.0 in 24 Hours. Puede ponerse en contacto con él a través de mitchell@4GuysFromRolla.com. o a través de su blog, que se puede encontrar en http://ScottOnWriting.NET.

Agradecimientos especiales a

Muchos revisores han evaluado esta serie de tutoriales. Los revisores principales de este tutorial han sido Liz Shulok, Dennis Patterson, Carlos Santos y Hilton Giesenow. ¿Le interesa revisar mis próximos artículos de MSDN? Si es así, escríbame a mitchell@4GuysFromRolla.com.