La descarga está en progreso. Por favor, espere

La descarga está en progreso. Por favor, espere

Creación de una Aplicación Multicapas con ASPNET 2005

Presentaciones similares


Presentación del tema: "Creación de una Aplicación Multicapas con ASPNET 2005"— Transcripción de la presentación:

1 Creación de una Aplicación Multicapas con ASPNET 2005
Miguel Ángel Niño Zambrano Adaptación de: Building Layered Web Applications with Microsoft ASP.NET Part

2 Objetivos Diseñar una aplicación muticapa, separando adecuadamente la presentación, el negocio y los datos. Diseñar, construir e implementar clases orientadas a objetos reusable de fácil mantenimiento. Crear objetos de negocio personalizados. Usar Visual Studio .Net 2005 para desarrollar la aflicción Web con este estilo arquitectónico.

3 Arquitectura de la Aplicación
Presentation layer – PL. Business Logic Layer – BLL. Data Access Layer – DAL. Objetivo primordial es poder cambiar una o mas capas si afectar las otras.

4 Arquitectura de la Aplicación
La PL pregunta al BLL por algún objeto, por ejemplo, el contacto de una persona. La BLL puede opcionalmente desarrollar alguna validación (por ejemplo, ¿Está habilitado el usuario actual para hacer ésta llamada?) y después llama al DAL. El DAL conecta a la base de datos y pregunta por un registro específico. Cuando el registro es encontrado, éste es retornado de la base de datos al DAL. El DAL empaqueta los datos de la base de datos en un objeto personalizado y lo retorna AL BLL. Finalmente, el BLL retorna el objeto al PL, donde podrías ser mostrado en una página Web.

5 El modelo Espagueti protected void Page_Load(object sender, EventArgs e) { if (!Page.IsPostBack) { string sql FirstName, LastName, MiddleName FROM ContactPerson WHERE Id = 1"; using (SqlConnection myConnection = new SqlConnection(ConfigurationManager.ConnectionStrings["NLayer"].ConnectionString)) using (SqlCommand myCommand = new SqlCommand(sql, myConnection)) myConnection.Open(); using (SqlDataReader myReader = myCommand.ExecuteReader()) if (myReader.Read()) { txtFirstName.Text = myReader.GetString(0); txtLastName.Text = myReader.GetString(1); if (!myReader.IsDBNull(2)) { txtMiddleName.Text = myReader.GetString(2); } myReader.Close(); myConnection.Close(); Dificultad para Escribir y recordar No se puede reutilizar éste código, hay que copiarlo y pegarlo todo.

6 El modelo Espagueti protected void btnSave_Click(object sender, EventArgs e) { string sqlBase ContactPerson SET FirstName='{0}', LastName='{1}', MiddleName ='{2}' WHERE Id = 1"; using (SqlConnection myConnection = new SqlConnection(ConfigurationManager.ConnectionStrings["NLayer"].ConnectionString)) string sql = String.Format(sqlBase, txtFirstName.Text, txtLastName.Text, txtMiddleName.Text); SqlCommand myCommand = new SqlCommand(sql, myConnection); myConnection.Open(); myCommand.ExecuteNonQuery(); myConnection.Close(); } Abre una conexión por cada operación SQL injection La capa de datos y lógica de negocio están revueltas

7 Controles SqlDataSource
<asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString="<%$ ConnectionStrings:NLayer %>" DeleteCommand="DELETE FROM [ContactPerson] WHERE [Id] InsertCommand="INSERT INTO [ContactPerson] ([FirstName], [MiddleName], [LastName], [DateOfBirth], [ContactPersonType]) VALUES SelectCommand="SELECT * FROM [ContactPerson]" UpdateCommand="UPDATE [ContactPerson] SET [FirstName] [MiddleName] [LastName] [DateOfBirth] [ContactPersonType] = @ContactPersonType WHERE [Id] OldValuesParameterFormatString="original_{0}" > <DeleteParameters> <asp:Parameter Name="original_Id" Type="Int32" /> </DeleteParameters> <UpdateParameters> <asp:Parameter Name="FirstName" Type="String" /> <asp:Parameter Name="MiddleName" Type="String" /> <asp:Parameter Name="LastName" Type="String" /> <asp:Parameter Name="DateOfBirth" Type="DateTime" /> <asp:Parameter Name="ContactPersonType" Type="Int32" /> <asp:Parameter Name="original_Id" Type="Int32" /> </UpdateParameters> <InsertParameters> <asp:Parameter Name="FirstName" Type="String" /> <asp:Parameter Name="MiddleName" Type="String" /> <asp:Parameter Name="LastName" Type="String" /> <asp:Parameter Name="DateOfBirth" Type="DateTime" /> <asp:Parameter Name="ContactPersonType" Type="Int32" /> </InsertParameters> </asp:SqlDataSource> CRUD Expone la lógica de los datos en la página No se puede reutilizar. Un nuevo campo debe actualizar el control. Opción 2: TableAdapter

8 Metodología de Desarrollo - DRA
Definición de la Arquitectura (Diagrama casos de uso reales, Despliegue, Aplicación, etc.) Análisis de Requerimientos (Ej. Análisis de Datos y Flujos de Datos, Diseño de Interfaces (HCI), story board, Análisis de Procesos) Diseño de la Aplicación Diseño de la Capa de Lógica de Negocio - BLL Diseño Clases Diseño del Modelo de Objetos Diseño de la Capa de Datos – DAL Diseño Separación Lógica de capas. (Namespaces) Diseño de la Base de Datos Implementación de la Aplicación Pruebas y Despliegue

9 Definición de la Arquitectura del Sistema
Estilo Arquitectónico: Capas y Patrones, Casos Reales.

10 Decisiones de Arquitectura
Aplicación Web cliente – Servidor. Estilo arquitectónico Multicapa: Presentación – Negocio – Persistencia. Uso de Patrones MVC y Proxy. Orientado a Objetos y Componentes. Uso de las mejores prácticas.

11 Introducción a la Aplicación de Ejemplo
Casos de Uso Reales

12 Análisis de Requerimientos
Lista de Reglas de Negocio, Informes, Reportes, Restricciones.

13 Requerimientos de la aplicación
Reglas del Negocio La aplicación debe ser capaz de ver una lista de todas las personas de contacto en el sistema. Además, ella debería ser capaz de crear nuevo, y cambiar y suprimir a personas de contacto existentes. La aplicación debe ser capaz de seleccionar a una persona de contacto específica, y luego conseguir una lista de sus direcciones asociadas. Además, ella debería ser capaz de crear nuevo y cambiar y suprimir direcciones existentes para la persona de contacto. La aplicación debe ser capaz de seleccionar a una persona de contacto específica, y luego conseguir una lista de sus direcciones de correo electrónico asociadas. Además, ella debería ser capaz de crear nuevo y cambiar y suprimir direcciones de correo electrónico existentes para la persona de contacto. El usuario del uso debe ser capaz de seleccionar a una persona de contacto específica, y luego conseguir una lista de sus números de teléfono asociados. Además, ella debería ser capaz de crear nuevo y cambiar y suprimir números de teléfono existentes para la persona de contacto.

14 Requerimientos de la aplicación
Consultas Presentar la información de los usuarios que coinciden con un nombre de usuario. Presentar la información de los contactos que están clasificados como amigos Informes Obtener un informe de todos los contactos y clasificado por tipo de contacto. Restricciones Un contacto no puede tener más de dos direcciones.

15 Diseño de la Aplicación
Capa lógica del Negocio BLL y Capa de Datos DAL

16 Diseño de Clases Del discurso anterior se debe diseñar las siguientes clases: ContactPerson Address Address PhoneNumber Mejores prácticas de diseño: ¿Qué acciones deberían realizar los objetos del negocio? Como una persona puede tener varios datos de la misma clase (números telefónicos y direcciones) entonces de necesita manejar una colección. Almacenar las clases por separado vs. un solo archivo de clase. Decide por separado con una clase que se encarga de administrar su conexión con los datos. Diseño Borrador

17 Diseño de Clases Métodos de ContactPerson que se compartirán entre los objetos del negocio: Métodos CRUD GetItem GetList Insert Update Delete Métodos específicos del Negocio Search Desactivate Clone Filter Por facilidad se usarán sólo los CRUD.

18 Diseño del Modelo de Objetos – Primera Aproximación
Usar una herramienta CASE. Existen dos enumeraciones ContactType y PersonType. La clase Address no tiene aún definido el tipo (type). Las clases PhoneNumber y Address no tienen CRUD. La clase ContactPerson no tiene propiedades para referenciar las clases PhoneNumber y Address. Es conveniente tener éstos tipos de datos en una colección: Ej. Address (List<Address>), PhoneNumber (List<PhoneNumber>). Para acceder al nombre completo debería crearse una propiedad adecuada FULLNAME. Los métodos Update e Insert se pueden unificar en un implementación UPsert.

19 Diseño del Modelo de Objetos – Segunda Aproximación
La clase ContactPerson tiene propiedades PhoneNumbers y Addresses, con los tipos de su respectiva colección. Para trabajar con una lista de personas se desarrollo un ContactPersonList. Se añadió un valor NotSet a las enumeraciones, diciendo que los valores no están determinados aún.

20 Diseño del Modelo de Objetos – Tercera Aproximación
Además de las clases anteriores se debe crea las clases manejadoras, según la decisión de diseño tomada. Cada una de éstas clases tienen una referencia a los objetos del negocio anterior. La implementación es: GetItem devuelve un caso de la clase, GetList devuelve una lista de los casos de la clase, mientras Save acepta un caso de la clase que debe ser almacenada en la base de datos. Finalmente, el método Delete acepta un caso de la clase también, conteniendo el artículo que debería ser suprimido. GetItem para el ContactPersonManager tiene una sobrecarga que le permite para determinar si usted quiere cargar sólo los datos básicos para un ContactPerson, o todos los datos asociados. Los GetList reciben sólo el id del ContactPerson y con éste ya conoce que desea traer Address, PhoneNumber, etc.

21 Diseño de la Capa de Datos DAL
Opciones TableAdapters Embedded Data Access Code. Separate DAL Classes. (Mejores prácticas) La última es la seleccionada e implica separadas DAL para cada uno de sus objetos de negocio: clase ContactPersonManager tiene a un colega ContactPersonDB, AddressManager tiene una clase AddressDB, etcétera. Las clase *DB interactúan directamente con la base de datos, con sus métodos CRUD. Los parámetros son los objetos del negocio (ContactPerson, Address, etc) o tipos simples según el caso.

22 Diseño del DAL En todas las clases el método Delete() retorna un bool para establecer el éxito o fracaso de la operación. El método Save() devuelve un int que sostiene el ID del registro que fue insertado o actualizado por el método. GetItem() y GetList() retornan objetos del negocio.

23 Diseño Separación lógica de Capas con Namespaces
Para separar claramente los objetos de negocio, la lógica de negocio y el acceso de datos, se ponen todos ellos en namespaces separados: Spaanjaars.ContactManager.BO: para los objetos de negocio. Spaanjaars.ContactManager.Bll: para la lógica de negocio. Spaanjaars.ContactManager.Dal: para los datos tienen acceso al código. Tanto la capa lógica de negocio como la capa de acceso de datos consiguen una referencia a los objetos en el BO namespace. Además, la capa de negocio consigue una referencia a la capa de acceso de datos para toda la interacción de datos. Los objetos del negocio se colocan en una capa diferente para evitar referencias circulares entre la capa del negocio y la de datos.

24 Diseño de la Base de Datos
Es conveniente usar una CASE. Seguir los pasos de un diseño adecuado de Base de datos. Las clases del negocio se mapean a tablas con sus respectivas relaciones. Desarrollar Procedimientos Almacenados adecuados a ser llamados por las clases *DB, teniendo en cuenta las interfaces definidas. Opcional: Usar un proyecto de BD.

25 Diseño Final La PL, por ejemplo una página de ASPX llamada ShowContactPerson.aspx, pide el BLL algún objeto, por ejemplo una persona de contacto. A través de un llamdo a ContactPersonManager. GetItem () obteniendo el ID de la persona de contacto. El BLL opcionalmente puede realizar alguna validación y luego envíar la petición al DAL llamando ContactManagerDB.GetItem (). El DAL se conecta a la base de datos y pide un registro específico. Ejecutando un procedimiento almacenado, como sprocContactPersonSelectSingleItem y pasándolo el ID de la persona de contacto. Cuando el registro es encontrado, es devuelto de la base de datos al DAL en un objeto de SqlDataReader. El DAL empaqueta los datos de base de datos en un ejemplar objeto ContactPerson el del BO y lo devuelve al BLL. Finalmente, el BLL devuelve el objeto de ContactPerson a la capa de Presentación, donde podría ser mostrado sobre una página Web por ejemplo.

26 Implementación de la Aplicación
Clases, propiedades, código de métodos, conexión a la BD, separación física de capas

27 Creación de Carpetas y Archivos de Clase
Separa físicamente las capas: BusinessLogic: Almacena archivos de la capa o namespace BLL. BusinessObject: Archivos de la capa o namespace BO. Contiene las Colecciones. DataAccess: Archivos de la capa o namespace Dal. Enums: Las Enumeraciones.

28 Crear Clases BO - Mejores Prácticas
Utilizar el concepto de #Region, para separar secciones de código. Especialmente establecer variables privadas y públicas. Conjuntos de métodos específicos.

29 Crear Clases BO - Mejores Prácticas
Correcto uso acceso a las variables privadas a través de propiedades. Es importante crear una propiedad type que retorne o asigne valores de los tipo enumerados a las propiedades de clase.

30 Crear Clases BLL Cada método de esta capa tiene la siguiente forma:
Una de la funcionalidades es validar RN: public static Address GetItem(int id) { return AddressDB.GetItem(id); } public static Address GetItem(int id, IPrincipal currentUser) { if (!currentUser.IsInRole(" AddressManagers")) throw new not allowed to call AddressManager.GetItem when you're not in the AddressManagers role"); } return AddressDB.GetItem(id); Opcionalmente se pueden cachear los objetos devueltos para no acceder nuevamente a la BD. Address my Address = AddressManager.GetItem(10, Context.User);

31 Crear las Clases de DAL Public Shared Function GetItem(ByVal id As Integer) As Address Dim myAddress As Address = Nothing Dim myConnection As SqlConnection = New SqlConnection(AppConfiguration.ConnectionString) Try Dim myCommand As SqlCommand = New SqlCommand("sprocAddressSelectSingleItem", myConnection) myCommand.CommandType = CommandType.StoredProcedure id) myConnection.Open Dim myReader As SqlDataReader = myCommand.ExecuteReader If myReader.Read Then myAddress = FillDataRecord(myReader) End If myReader.Close Finally CType(myReader, IDisposable).Dispose() End Try myConnection.Close CType(myConnection, IDisposable).Dispose() Return myAddress End Function

32 Crear las Clases de DAL Private Shared Function FillDataRecord(ByVal myDataRecord As IDataRecord) As Address Dim my Address As Address = New Address my Address.Id = myDataRecord.GetInt32(myDataRecord.GetOrdinal("Id")) my Address. = myDataRecord.GetString(myDataRecord.GetOrdinal(" ")) my Address.Type = CType(myDataRecord.GetInt32(myDataRecord.GetOrdinal(" Type")), ContactType) my Address.ContactPersonId = myDataRecord.GetInt32(myDataRecord.GetOrdinal("ContactPersonId")) Return my Address End Function ‘la interfaz IDataRecord da un acceso fuertemente tipado a los valores ‘dentro de cada registro. Permite independizar el procedimiento del ‘proveedor de datos Ej. SqlDataReader, OleDbDataReader. Para el manejo de Nulos se aconseja lo siguiente: if (!myDataRecord.IsDBNull(myDataRecord.GetOrdinal("ZipCode"))) { myAddress.ZipCode = myDataRecord.GetString(myDataRecord.GetOrdinal("ZipCode")); }

33 Crear las Clases de DAL Public Shared Function GetList(ByVal contactPersonId As Integer) As AddressList Dim tempList As AddressList = Nothing Dim myConnection As SqlConnection = New SqlConnection(AppConfiguration.ConnectionString) Try Dim myCommand As SqlCommand = New SqlCommand("sproc AddressSelectList", myConnection) myCommand.CommandType = CommandType.StoredProcedure contactPersonId) myConnection.Open() Dim myReader As SqlDataReader = myCommand.ExecuteReader If myReader.HasRows Then tempList = New AddressList While myReader.Read tempList.Add(FillDataRecord(myReader)) End While End If myReader.Close() Finally CType(myReader, IDisposable).Dispose() End Try CType(myConnection, IDisposable).Dispose() Return tempList End Function Para el manejo de Nulos se aconseja lo siguiente: if (!myDataRecord.IsDBNull(myDataRecord.GetOrdinal("ZipCode"))) { myAddress.ZipCode = myDataRecord.GetString(myDataRecord.GetOrdinal("ZipCode")); }

34 Creación de Clases de Listas de Objetos de Negocio
Imports System Imports System.Collections.Generic namespace Spaanjaars.ContactManager.BO ''' <summary> ''' The AddressList class is designed to work with lists of instances of Address, hacer su uso tipado y mas fácil de usar. ''' </summary> Public Class AddressList Inherits List(Of Address) Public Sub New() End Sub End Class End Namespace ‘El llamado en la funcion GetList seria: ‘Dim tempList As AddressList = Nothing

35 Creación de Procedimientos Almacenados en la BD
CREATE PROCEDURE sproc AddressSelectSingleItem @id int AS SELECT Id, , Type, ContactPersonId FROM Address WHERE Id

36 Clases DAL Método Save()
Public Shared Function Save(ByVal my Address As Address) As Integer Dim result As Integer = 0 Dim myConnection As SqlConnection = New SqlConnection(AppConfiguration.ConnectionString) Try Dim myCommand As SqlCommand = New SqlCommand("sproc AddressInsertUpdateSingleItem", myConnection) myCommand.CommandType = CommandType.StoredProcedure If my Address.Id = -1 Then DBNull.Value) Else my Address.Id) End If my Address. ) my Address.Type) my Address.ContactPersonId) Dim returnValue As DbParameter returnValue = myCommand.CreateParameter returnValue.Direction = ParameterDirection.ReturnValue myCommand.Parameters.Add(returnValue) myConnection.Open() myCommand.ExecuteNonQuery() result = Convert.ToInt32(returnValue.Value) myConnection.Close() Finally CType(myConnection, IDisposable).Dispose() End Try Return result End Function

37 Creación de Procedimientos Almacenados en la BD
CREATE PROCEDURE sproc AddressInsertUpdateSingleItem @id int, @ nvarchar (100), @ Type int, @contactPersonId int AS int IF IS NULL) -- New Item BEGIN INSERT INTO Address ( , Type, ContactPersonId ) VALUES @ , @ Type, @contactPersonId = SCOPE_IDENTITY() END ELSE BEGIN UPDATE Address SET Type ContactPersonId WHERE Id IF != 0) RETURN -1 GO

38 Elementos de Implementación de la clase ContactPerson
Private _strAddresses As AddressList = New AddressList Private _phoneNumbers As PhoneNumberList = New PhoneNumberList Private _ Addresses As AddressList = New AddressList Public ReadOnly Property FullName() As String Get Dim tempValue As String = _strFirstName If Not String.IsNullOrEmpty(_stMiddleName) Then tempValue += " " + MiddleName End If tempValue += " " + _strLastName Return tempValue End Get End Property

39 Elementos de Implementación de la clase ContactPerson
Public Shared Function GetItem(ByVal id As Integer) As ContactPerson Return GetItem(id, False) End Function Public Shared Function GetItem(ByVal id As Integer, ByVal getContactRecords As Boolean) As ContactPerson Dim myContactPerson As ContactPerson = ContactPersonDB.GetItem(id) If Not (myContactPerson Is Nothing) AndAlso getContactRecords Then myContactPerson.Addresses = AddressDB.GetList(id) myContactPerson. Addresses = AddressDB.GetList(id) myContactPerson.PhoneNumbers = PhoneNumberDB.GetList(id) End If Return myContactPerson

40 Elementos de Implementación de la clase ContactPerson
Public Shared Function Save(ByVal myContactPerson As ContactPerson) As Integer Dim myTransactionScope As TransactionScope = New TransactionScope Try Dim contactPersonId As Integer = ContactPersonDB.Save(myContactPerson) For Each myAddress As Address In myContactPerson.Addresses myAddress.ContactPersonId = contactPersonId AddressDB.Save(myAddress) Next For Each my Address As Address In myContactPerson. Addresses my Address.ContactPersonId = contactPersonId AddressDB.Save(my Address) For Each myPhoneNumber As PhoneNumber In myContactPerson.PhoneNumbers myPhoneNumber.ContactPersonId = contactPersonId PhoneNumberDB.Save(myPhoneNumber) myContactPerson.Id = contactPersonId myTransactionScope.Complete Return contactPersonId Finally CType(myTransactionScope, IDisposable).Dispose() End Try End Function

41 Elementos de Implementación de la clase ContactPerson
CREATE PROCEDURE sprocContactPersonDeleteSingleItem @id int AS BEGIN TRAN DELETE FROM Address WHERE ContactPersonId IF <> 0 BEGIN ROLLBACK TRAN RETURN -1 END Address DELETE FROM PhoneNumber WHERE ContactPersonId IF <> 0 BEGIN ROLLBACK TRAN RETURN -1 END ContactPerson Id COMMIT TRAN

42 Uso de la clase ContactPerson
ContactPerson myContactPerson = new ContactPerson(); myContactPerson.FirstName = "Imar"; myContactPerson.LastName = "Spaanjaars"; myContactPerson.DateOfBirth = new DateTime(1971, 8, 9); myContactPerson.Type = PersonType.Family; Address myAdress = new Address(); myAdress.Street = "Some Street"; myAdress.HouseNumber = "Some Number"; myAdress.ZipCode = "Some Zip"; myAdress.City = "Some City"; myAdress.Country = "Some Country"; myContactPerson.Addresses.Add(myAdress); Address my Adress = new Address(); my Adress. = my Adress.Type = ContactType.Personal; myContactPerson. Addresses.Add(my Adress); PhoneNumber myPhoneNumber = new PhoneNumber(); myPhoneNumber.Number = " "; myPhoneNumber.Type = ContactType.Personal; myContactPerson.PhoneNumbers.Add(myPhoneNumber); ContactPersonManager.Save(myContactPerson);

43 Implementación de la Aplicación Web - DAL
Web Forms y controles, Web Config.

44 Resumen de la Capas

45 Archivos de la Aplicación Web
App_Data and NLayer.mdf: BD Usada en la aplicación. App_Themes and Css\Styles.css: Contirne un tema para el gridView. Y el archivo de estilos. Web.config: Configuraciones de la aplicación.

46 Formulario Default.aspx
Cree una nueva página e intercambie a la vista de diseño. Añada un GridView a la página. Abra el panel de Tareas del GridView y Escoge la Fuente de Datos select <New data source>. En el asistente de configuración del DataSource, Seleccione el Objeto y click OK. Escoja un Objeto De negocio, asegúrese muestre sólo los componentes de datos esten chequeados y luego escoger el objeto apropiado de negocio de la lista. En el caso, escoger: Spaanjaars. ContactManager. Bll. ContactPersonManager: Define Data Methods window asegúrese que sobre la etiqueta escogida sea el GetList(). Después, limpie la selección sobre la etiqueta de Update, esta página no necesita un método de Update. Dejar las demás etiquetas como están.

47 Atributos para el Diseñador
<DataObjectMethod> establece el método por defecto para traer datos del AddressList. <DataObjectFieldAttribute(True, True, False)> Permite que la propiedad la use el asistente. <DataObjectMethod(DataObjectMethodType.Select, True)> _ Public Shared Function GetList(ByVal contactPersonId As Integer) As AddressList Return AddressDB.GetList(contactPersonId) End Function <DataObjectFieldAttribute(True, True, False)> _ Public Property Id() As Integer Get Return _intId End Get Set(ByVal value As Integer) _intId = Value End Set End Property

48 Vista de Presentación Todos los contactos

49 Añadir botones para permitir al usuario nuevo, editar, borrar
<asp:ButtonField CommandName="Edit" Text="Edit"/> <asp:TemplateField ShowHeader="False"> <ItemTemplate> <asp:LinkButton ID="LinkButton1" runat="server" CausesValidation="False" CommandName="Delete" Text="Delete" OnClientClick="return confirm('Are you sure you want to delete this contact person?');"> </asp:LinkButton> </ItemTemplate> </asp:TemplateField>

50 Establecer un método que captura el comando seleccionado
<asp:GridView ID="gvContactPersons" runat="server" AutoGenerateColumns="False" DataSourceID="odsContactPersons" DataKeyNames="Id" OnRowCommand="gvContactPersons_RowCommand" AllowPaging="True" CellPadding="4" GridLines="None"><Columns> ....</Columns></asp:GridView>

51 Vista de Edición de Datos


Descargar ppt "Creación de una Aplicación Multicapas con ASPNET 2005"

Presentaciones similares


Anuncios Google