Mostrando las entradas con la etiqueta Chinook. Mostrar todas las entradas
Mostrando las entradas con la etiqueta Chinook. Mostrar todas las entradas

Construyendo una UI

¿que tan difícil puede ser construir una interface de usuario web para alguien que no sabe mucho del tema?

Poco. Siguiendo con la idea de siempre, delegamos esto a algún experto, el único detalle en este caso es que no conozco ninguno que sea de uso gratuito y que llegue a los niveles de los productos comerciales.

En este caso vamos a usar DevExpress en su modo evaluación, por lo que vamos a tener una leyenda en la parte superior de nuestras páginas advirtiendo esto.

Como primer UI vamos a hacer un sitio web, vamos a ver como con pocas líneas de nuestro código logramos mucha funcionalidad.

El objetivo

CustomersView

Este formulario vamos a dividirlo en dos partes

  • Master Page: el título (“Digital Media Store”), el combo para seleccionar la apariencia, el menú de la izquierda y el pié de página.
  • CustomersView: el filtro por país y la grilla.

En esta última es que vamos a centrarnos, como podemos ver, además del filtro por país, la grilla tiene la funcionalidad de filtro por cada una de las columnas (eso es la primer fila con textbox vacios). También tiene un botones para editar, crear y eliminar customers. tiene la opción de agrupar por columnas (“Drag a column header here to group by that column”), ordenar, pagina los datos, y todo esto con AJAX.

pregunta: ¿cuantas líneas de código necesitamos para hacer esto, incluyendo los formularios de edición/alta?. Respuesta: 23 líneas en el aspx, 0 (cero) en el code behind y 16 de código C# en una clase. Veámoslas…

El código

El filtro por país

un label, un textbox y un button que solo dispara un postback

<table>
<
tr>
<
td><dxe:ASPxLabel ID="lblContry" Text="Country" runat="server" ></dxe:ASPxLabel></td>
<
td><dxe:ASPxTextBox ID="txtCountry" runat="server" Text="France" Width="170px"></dxe:ASPxTextBox></td>
<
td><dxe:ASPxButton ID="btnSearch" runat="server" Text="Search"></dxe:ASPxButton></td>
</
tr>
</
table>

La grilla:

una grilla de DevExpress

<dxwgv:ASPxGridView ID="gridCustomers" runat="server" DataSourceID="odsCustomers" KeyFieldName="Id" Width="100%">
<
Columns>
<
dxwgv:GridViewCommandColumn>
<
EditButton Visible="true" />
<
NewButton Visible="True" />
<
DeleteButton Visible="True" />
</
dxwgv:GridViewCommandColumn>
<
dxwgv:GridViewDataColumn FieldName="FirstName" />
<
dxwgv:GridViewDataColumn FieldName="LastName" />
<
dxwgv:GridViewDataColumn FieldName="Country" />
<
dxwgv:GridViewDataColumn FieldName="State" />
<
dxwgv:GridViewDataColumn FieldName="City" />
</
Columns>
<
Settings ShowFilterRow="True" ShowGroupPanel="true"/>
<
SettingsBehavior ConfirmDelete="true" />
<
SettingsEditing EditFormColumnCount="1" Mode="PopupEditForm" PopupEditFormModal="true" PopupEditFormHorizontalAlign="Center" PopupEditFormVerticalAlign="Middle" />
</
dxwgv:ASPxGridView>

veamos las propiedades significativas:


  • DataSourceID para vincularnos al data source,
  • KeyFieldName es en nombre de la property que identifica a cada objeto (en este caso es Id)
  • Columns es la lista de columnas que va a tener la grilla, empezando por las de comando: EditButton, NewButton y DeleteButton y siguiente por las propiedades que queremos mostrar en la grilla y/o en los formularios de edición.
  • ShowFilterRow muestra la primer fila donde están los campos para el filtrado.
  • ShowGroupPanel muestra el panel de agrupamiento.
  • ConfirmDelete pide una confirmación al intentar borrar un registro.
  • Mode indicamos el modo en que queremos que aparezca la pantalla de alta y edición de nuevos customers.
  • PopupEditFormModal indica que el popup va a ser modal, es decir que no se va a poder usar otra parte de la página mientras se esté editando.

y esto es todo para obtener la funcionalidad citada: filtrar, editar, crear, eliminar, agrupar, ordenar y paginar (estas dos últimas están activas por default).

El DataSource

un ObjectDataSource de asp.net:

<asp:ObjectDataSource ID="odsCustomers" runat="server" DataObjectTypeName="DMS.DTOs.CustomerDTO, DMS.DTOs" TypeName="DMS.Web.DataSources.CustomerDataSource"
SelectMethod="Select" InsertMethod="Save" UpdateMethod="Save" DeleteMethod="Delete">
<
SelectParameters>
<
asp:ControlParameter Name="country" PropertyName="Text" ControlID="txtCountry" />
</
SelectParameters>
</
asp:ObjectDataSource>

veamos las propiedades significativas:


  • DataObjectTypeName indica el tipo de objeto que vamos listar en la grilla, en este caso un CustomerDTO.
  • TypeName es el tipo de objeto que vamos a usar como datasource y los métodos para cada acción: SelectMethod, InsertMethod, UpdateMethod, DeleteMethod.
  • SelectParameters son los parámetros que tiene el método asociado a SelectMethod, en este caso se llama country y el valor lo tomamos de la propiedad “Text” del control “txtCountry”.

El código del DataSource:
public class CustomerDataSource
{
private readonly ICustomerService service = ServiceLocatorHelper.GetService<ICustomerService>();

public IList<CustomerDTO> Select(string country)
{
ResultListDTO<CustomerDTO> result = service.GetCustomersByContry(country);
if (result.Exception != null)
throw result.Exception;
return result.Items;
}

public void Save(CustomerDTO dto)
{
service.Save(dto);
}

public void Delete(CustomerDTO dto)
{
service.DeleteById(dto.Id);
}
}

Aquí simplemente hacemos llamadas al servicio de manejo de customers y manejamos los valores de retorno. Aún me falta una vuelta de rosca para el manejo de mensajes, pero en breve espero que esté.

Y esto es solo una mínima muestra de lo que se puede hacer con DevExpress

Persistencia del dominio de nuestro ejemplo

Nuestro dominio de objetos

Domain

Nuestro modelo relacional (tomado de Chinook 1.1)

modelo relacional

¿Cómo lo persistimos?

¿Cómo guardamos y recuperamos nuestro dominio de objetos de la una base de datos?... como se imaginarán no estoy pensando en escribir los insert, update, delete o select necesarios sino que, siguiendo en tren de delegar responsabilidades a expertos, usamos un ORM para la persistencia de nuestras entidades, nuestra primera implementación va a ser con nhibernate (nh). Esto quiere decir que vamos a hacer una implementación de DAOs con nh que se va a llamar DAOs.NH.

¿Cómo hacemos para no quedar “atados” a NHibernate?

Sólo vamos a usar (referenciar) nhibernate desde las implementaciones correspondientes (al momento de este post DAOs.NH, DAOs.NH.Castle y BEs.Validations.NHV). Es decir que no vamos a tener referencias a Nhibernate desde otras implementaciones como Models.Impl o Services.Impl. Ni decir que el Core de nuestra aplicación no va a conocerlo (esto es, los contratos, las BEs y los DTOs).

Para ver como usar NHibernate de la forma tradicional (hasta el momento) pueden ver la documentación oficial o los muchos ejemplos que hay en internet.

Configurando NH con ConfOrm

En este post vamos a ver la configuración de NHibernate mediante ConfOrm para nuestro dominio, y para esto vamos a hacer algunos ajustes a la base de datos (db). Es verdad que también podríamos indicar en los mappings (o a ConfOrm) las diferencias entre el modelo de objetos y el modelo relacional, pero como considero que la base de datos también es parte de nuestra aplicación y por lo tanto es factible de refactorización es que decido modificarla. Los scripts para esto están en la carpeta scripts de nuestro código de ejemplo. Dichos cambios consisten en renombrar algunos campos y una tabla, no modificamos en concepto el diseño de la db.

Ahora bien, vamos al código:

ObjectRelationalMapper orm = new ObjectRelationalMapper();

orm.TablePerClass<Album>();
orm.TablePerClass<Artist>();
orm.TablePerConcreteClass<Person>();
orm.TablePerClass<Genre>();
orm.TablePerClass<Invoice>();
orm.TablePerClass<InvoiceLine>();
orm.TablePerClass<MediaType>();
orm.TablePerClass<Playlist>();
orm.TablePerClass<Track>();

orm.Bag<Playlist>(pl => pl.Tracks);
orm.ManyToMany<Playlist, Track>();

Mapper mapper = new Mapper(orm);

Type type = typeof(Track);
HbmMapping mapping = mapper.CompileMappingFor(type.Assembly.GetTypes());

Configuration configuration = new Configuration();
configuration.Configure();

configuration.AddDeserializedMapping(mapping, "DMS_BEs");

return configuration;

Como podemos ver, el objetivo de esto es llegar al objeto Configuration de nh, el cual antes configurábamos a partir de los .hbm.xml embebidos en el assembly o con attributes sobre nuestro dominio. Para ver las distinas opciones de configuración de ConfOrm pueden revisar el blog de Fabio que seguro encontrarán mucho.

¿y como quedaron estos mappings?

No es necesario escribir los mappings en un archivo, pero si queremos verlos podemos:

<?xml version="1.0" encoding="utf-8"?>
<hibernate-mapping xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" namespace="DMS.BEs" assembly="DMS.BEs" xmlns="urn:nhibernate-mapping-2.2">
    <class name="Invoice">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <many-to-one name="Customer" />
        <property name="InvoiceDate" />
        <property name="BillingAddress" />
        <property name="BillingCity" />
        <property name="BillingState" />
        <property name="BillingCountry" />
        <property name="BillingPostalCode" />
        <bag name="Lines" inverse="true" cascade="all,delete-orphan">
            <key column="Invoice" on-delete="cascade" />
            <one-to-many class="InvoiceLine" />
        </bag>
    </class>
    <class name="Track">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <property name="Name" />
        <many-to-one name="Album" />
        <many-to-one name="MediaType" />
        <many-to-one name="Genre" />
        <property name="Composer" />
        <property name="Milliseconds" />
        <property name="Bytes" />
        <property name="UnitPrice" />
    </class>
    <class name="Artist">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <property name="Name" />
    </class>
    <class name="Genre">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <property name="Name" />
    </class>
    <class name="MediaType">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <property name="Name" />
    </class>
    <class name="Playlist">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <property name="Name" />
        <bag name="Tracks">
            <key column="playlist_key" />
            <many-to-many class="Track" />
        </bag>
    </class>
    <class name="InvoiceLine">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <many-to-one name="Invoice" />
        <many-to-one name="Track" />
        <property name="UnitPrice" />
        <property name="Quantity" />
    </class>
    <class name="Album">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <many-to-one name="Artist" />
        <property name="Title" />
        <bag name="Tracks" inverse="true" cascade="all,delete-orphan">
            <key column="Album" on-delete="cascade" />
            <one-to-many class="Track" />
        </bag>
    </class>
    <class name="Person" abstract="true">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <property name="FirstName" />
        <property name="LastName" />
        <property name="Address" />
        <property name="City" />
        <property name="State" />
        <property name="Country" />
        <property name="PostalCode" />
        <property name="Phone" />
        <property name="Fax" />
        <property name="Email" />
    </class>
    <union-subclass name="Employee" extends="Person">
        <property name="Title" />
        <property name="BirthDate" />
        <property name="HireDate" />
        <many-to-one name="ReportsTo" />
    </union-subclass>
    <union-subclass name="Customer" extends="Person">
        <property name="Company" />
        <many-to-one name="SupportRepresentant" />
    </union-subclass>
</hibernate-mapping>