Gesti.n de concurrencia en ADO.NET

Anuncio
Por Jesús López Méndez
(SqlRanger)
Gestión de concurrencia en ADO.NET
La concurrencia, en un entorno multiusuario, es siempre una cuestión problemática, pero si además se trata de un entorno desconectado como el que se
usa en ADO.NET con sus DataSets y DataAdapters, la problemática es aún mayor
debido a la propia naturaleza desconectada del entorno.
en primer lugar, cual es esa problemática y
<< Veamos,posteriormente
qué alternativas tenemos para
tratarla. El problema fundamental que se nos
plantea son los conflictos de concurrencia. Un
conflicto de concurrencia se produce cuando un
usuario modifica un registro de una tabla de una
base de datos y ese registro ha cambiado desde
la última vez que lo leyó. Por ejemplo, consideremos la siguiente secuencia de sucesos:
• Los usuarios A y B leen el registro R1 de
la base de datos cargándolo en un DataSet.
• El usuario A modifica R1
• El usuario A guarda R1 en la base de datos.
• El usuario B modifica R1
• El usuario B guarda R1 en la base de datos.
Los conflictos de concurrencia no se producen solamente al actualizar un registro porque
otro usuario lo haya modificado, también ocurren si el registro ha sido eliminado por otro usuario. Asimismo, tienen lugar cuando un usuario
intenta eliminar un registro que ha sido modificado e incluso que ha sido eliminado. Con la
inserción, sin embargo, es evidente que no se producen conflictos de concurrencia, ya que es imposible que otro usuario pueda modificar un registro que aún no existe en la base de datos. En definitiva, los conflictos de concurrencia pueden producirse:
• Al modificar un registro
• Al eliminar un registro
Y la causa del conflicto puede ser porque
dicho registro:
<<dotNetManía
• Ha sido modificado desde la última vez que
se leyó.
• Ha sido eliminado desde la última vez que
se leyó.
20
• El usuario B recibe una excepción
DBConcurrencyException, indicando un conflicto de concurrencia al haber sido modificado R1 desde la última vez que B lo leyó.
Otro aspecto básico acerca de los conflictos
de concurrencia es la forma de detectarlos. La
técnica de detección se basa fundamentalmente
en incluir en la cláusula WHERE de la instrucción UPDATE o DELETE el valor original de
los campos, es decir, el valor que tenían los campos del registro cuando se leyeron de la base de
datos. Pongamos un ejemplo para aclarar ideas.
<< dnm.plataforma.ado.net
pos en la cláusula SET, excepto el
IdEmpleado que es autonumérico y por
tanto de sólo lectura. Esto viene a
CREATE TABLE Empleados (
IdEmpleado INT IDENTITY(1,1) PRIMARY KEY,
DNI VARCHAR(12) NOT NULL UNIQUE,
Nombre VARCHAR(50) NOT NULL,
Apellidos VARCHAR(50) NOT NULL
)
El comando UPDATE que detecta conflictos de concurrencia sería el
siguiente:
suponer que se actualizarán todos los
campos en la tabla, independientemente de si se han modificado o no,
UPDATE Empleados
SET
DNI=@DNI, Nombre=@Nombre, Apellidos=@Apellidos
WHERE IdEmpleado=@Original_IdEmpleado AND DNI=@Original_DNI AND
Nombre=@Original_Nombre AND Apellidos=@Original_Apellidos
Como veis, están todos los valores
originales en la cláusula WHERE de
esta instrucción parametrizada. De
esta manera, si ha cambiado alguno
de los campos, no se cumplirá la condición, y por tanto la instrucción no
actualizará ningún registro, o lo que
es lo mismo, el número de registros
afectados será cero. Solamente la instrucción tendrá éxito, o sea, actualizará el registro, si éste no ha cambiado. Así es como ADO.NET detecta
los conflictos de concurrencia, concretamente un DataAdapter lanzará
una excepción DBConcurrencyException
cuando el comando de actualización
afecte a cero registros. Observad que
este comando incluye todos los cam-
lo que implica una falta de optimización.
Sin embargo, es posible que no nos
interese detectar conflictos de concurrencia y que queramos que la actualización se lleve a cabo independientemente de si el registro ha sido modificado o no desde la última vez que se
leyó. En ese caso sólo incluiríamos la
clave primaria en la cláusula WHERE.
La instrucción UPDATE sería la
siguiente:
Aún así, podríamos obtener un
conflicto de concurrencia, pero sólo
en el caso de que haya sido eliminado
el registro. Un inconveniente de esta
opción es que es posible perder modificaciones. Si por ejemplo, los usuarios A y B leen el empleado 10, el
usuario A modifica su nombre y lo
guarda, y luego B modifica el apellido y lo guarda, las dos actualizaciones
tienen éxito, pero la modificación que
hizo A se pierde, ya que el nombre es
sobrescrito con el valor que leyó B.
En ciertos sistemas, esta posible pérdida de modificaciones es inaceptable
y por tanto habría que elegir otra
opción.
Otra alternativa sería incluir en la
cláusula SET sólo los campos que se
han modificado. De esta manera, aunque sólo incluyéramos la clave primaria en la cláusula WHERE, no se
perderían las modificaciones. También
es una opción interesante incluir en
la cláusula SET sólo los campos modificados, y en la cláusula WHERE la
clave primaria más el valor original de
los campos que han cambiado, así el
conflicto de concurrencia que detectaríamos sería en el caso de que otro
usuario hubiera modificado alguno de
los campos que han sido modificados
o en el caso de eliminación. Por ejemplo, si los usuarios A y el B leen el
empleado 10, el usuario A modifica su
UPDATE Empleados
SET
DNI=@DNI, Nombre=@Nombre, Apellidos=@Apellidos
WHERE IdEmpleado=@Original_IdEmpleado
Los conflictos de concurrencia no se producen
solamente al actualizar un registro porque otro usuario
lo haya modificado, también ocurren si el registro
ha sido eliminado por otro usuario
nombre y lo guarda y el usuario B
modifica su nombre y lo guarda, el
usuario B recibe un conflicto de concurrencia. Pero si lo que ocurre es que
el usuario A modifica el nombre y el
B el apellido, no hay conflicto de concurrencia y las dos actualizaciones tienen éxito.
Una última alternativa para la cláusula WHERE es incluir la clave primaria más el valor original de un campo de tipo TimeStamp que por supuesto tendría que formar parte de la tabla.
El funcionamiento es equivalente a
incluir los valores originales de todos
<<dotNetManía
Supongamos que estamos trabajando
con la siguiente tabla en una base de
datos de SQL Server:
21
<< dnm.plataforma.ado.net
los campos, pero resulta más eficiente ya que la instrucción es más corta, reduciéndose el tráfico de
red y reduciendo el trabajo del procesador de consultas del servidor de base de datos. Un campo de
tipo TimeStamp en SQL Server es una especie de
autonumérico de 64 bits único en toda la base de
datos. No puede haber dos registros en una base de
<<dotNetManía
La técnica de detección se basa fundamentalmente
en incluir en la cláusula WHERE
de la instrucción UPDATE o DELETE
el valor original de los campos
22
datos con el mismo valor de TimeStamp, incluso
aunque pertenezcan a distintas tablas. Cada vez que
se modifica un registro que tiene un campo
TimeStamp, el valor del campo también cambia.
Debido a esto y si usamos esta alternativa, después
de modificar un registro, sería necesario volver a
leer el campo TimeStamp para poder realizar más
modificaciones en el mismo registro.
Detectar conflictos de concurrencia en la eliminación es similar a la actualización, con la salvedad de que en este caso sólo podemos jugar con la
cláusula WHERE de la instrucción DELETE.
Podríamos incluir sólo la clave primaria, en cuyo
caso sólo obtendremos conflictos de concurrencia
cuando otro usuario haya eliminado el registro. En
la mayoría de los casos, este conflicto sencillamente lo podríamos ignorar. También podríamos incluir
todos los valores originales de los campos o la clave primaria más el TimeStamp, en cuyo caso recibiremos un conflicto de concurrencia cuando otro
usuario haya modificado o eliminado el registro.
Una vez que tenemos decidido cómo vamos a
detectar los conflictos de concurrencia y cómo
vamos a hacer las actualizaciones y eliminaciones,
hemos de decidir cómo los vamos a tratar, o sea,
qué acciones vamos a tomar en el caso de un conflicto de concurrencia. Cada conflicto de concurrencia lo trataremos de manera diferente en función de si se ha producido al hacer una actualización o al realizar una eliminación y en función de
la causa del conflicto, esto es, si ha sido porque
otro usuario lo ha modificado o porque lo ha eliminado.
Empecemos primero por los conflictos que se
producen al actualizar. Si la causa es que otro usuario lo ha modificado podríamos tener las siguientes alternativas:
• Descartar las modificaciones y refrescar el registro
volviéndolo a leer de la base de datos. Al usuario
le avisaríamos del conflicto de concurrencia y
le daríamos la oportunidad de volver a hacer
las modificaciones.
• Refrescar sólo los valores originales sin descartar las
modificaciones. Al usuario le avisaríamos del conflicto. Entonces él tendría la oportunidad de
ver las modificaciones deshaciendo cambios o
de volver a guardar con lo que forzaría la actualización.
• Directamente forzar la actualización. Esto se
conoce como la técnica “el último que llega
gana”. En realidad esta acción no es una respuesta a un conflicto de concurrencia, ya que
para llevarla a cabo incluiríamos únicamente
la clave primaria en la cláusula WHERE, no
detectándose conflictos de concurrencia por
modificación.
Si la causa es que otro usuario lo ha eliminado,
las alternativas serían las siguientes:
• Volver a insertar el registro en la base de datos. En
el caso de que tengamos un autonumérico en
la tabla no sería posible volver a insertar el
registro exactamente igual a como era anteriormente.
• Eliminarlo del DataSet. Esta es la opción que
más se suele utilizar.
Detectar la causa del conflicto, al igual que
refrescar un registro, puede realizarse volviendo a
leer tal registro de la base de datos basándose en la
clave primaria, pero si la clave primaria puede cambiar, esta técnica no sirve para su propósito ya que
si ésta ha cambiado no es posible identificar el registro y no es posible determinar si el conflicto de concurrencia ha ocurrido por modificación o por eliminación. Por eso sería recomendable usar claves
primarias artificiales como autonuméricos o
GUID’s.
En cuanto a los conflictos de concurrencia que
se producen al eliminar un registro, podríamos tener
las siguientes alternativas cuando la causa es por
modificación:
• Deshacer la eliminación y refrescar el registro. Al
usuario le informaríamos del conflicto y tendría la posibilidad de volverlo a eliminar después de haber visto los cambios realizados.
<< dnm.plataforma.ado.net
• Forzar la eliminación. En realidad
esta acción no es una respuesta a
un conflicto de concurrencia, ya
que para llevarla a cabo incluiríamos únicamente la clave primaria
en la cláusula WHERE con lo que
no se detectan conflictos de concurrencia por modificación.
Figura1. Opciones avanzadas de generación de instrucciones SQL.
del valor actual o del valor original.
Antes de poder invocar al método
Update de un DataAdapter tenemos
que configurarlo correctamente, esto
es, tenemos que establecerle los
comandos de actualización. Para configurar un DataAdapter tenemos tres
alternativas:
• Usar el asistente para la configuración del DataAdapter
• Usar un CommandBuilder
• Configurarlo manualmente
escribiendo nosotros mismos el
código
Para usar el asistente, sólo tenemos que arrastrar un DataAdapter
de la ficha datos del cuadro de
herramientas a nuestro formulario
o componente y seguir sus instrucciones. En el paso “Generar las instrucciones SQL” tenemos un botón
“Opciones avanzadas” que nos pre-
senta el cuadro de diálogo de la
figura 1.
Como vemos, el asistente puede
generar por nosotros los comandos de
actualización INSERT, UPDATE y
DELETE. Si elegimos “Usar concurrencia optimista”, el asistente incluirá en la cláusula WHERE de las instrucciones UPDATE y DELETE el
valor original de todos los campos del
registro. Mientras que si no activamos
esa casilla de verificación, la cláusula
WHERE sólo incluirá la clave primaria. Si elegimos “Actualizar el conjunto de datos” el asistente añade una
instrucción SELECT a los comandos
de actualización para refrescar el registro. En cualquier caso, la instrucción
UPDATE incluye en la cláusula SET
todos los campos.
Esta sería la instrucción UPDATE para nuestra tabla de ejemplo
usando concurrencia optimista. Ver
tabla 1.
UPDATE Empleados
SET
DNI=@DNI, Nombre=@Nombre, Apellidos=@Apellidos
WHERE IdEmpleado=@Original_IdEmpleado AND DNI=@Original_DNI AND
Nombre=@Original_Nombre AND Apellidos=@Original_Apellidos
Tabla 1
<<dotNetManía
Por último, el conflicto de concurrencia que se produce al eliminar un
registro que ha sido eliminado, generalmente puede tratarse sencillamente ignorando el conflicto y eliminando definitivamente el registro del
DataSet.
Como hemos visto, existen varias
alternativas para detectar y tratar los
conflictos de concurrencia. Veamos
ahora qué nos ofrece ADO.NET en
este sentido.
En ADO.NET tenemos una serie
de clases, los DataAdapters, que son
los encargados de revertir las modificaciones realizadas en un DataSet
sobre la base de datos mediante su
método Update. Los DataAdapters tienen tres propiedades: DeleteCommand,
UpdateCommand e InsertCommand que
son los comandos de actualización.
Estos comandos son parametrizados,
de manera que sirvan para todas las filas
de un DataTable. Cuando invocamos al
método Update de un DataAdapter, éste
recorre todas las filas del DataTable, y
si la fila es una fila eliminada, ejecuta
el DeleteCommand; si la fila es una fila
modificada, invoca el UpdateCommand;
y si es una fila nueva, invoca al
InsertCommand. Si al invocar al UpdateCommand o al DeleteCommand, el
número de registros afectados es cero,
el DataAdapter lanza una excepción
DBConcurrencyException indicando que
se ha producido un conflicto de concurrencia. Antes de invocar un comando de actualización, el DataAdapter
establece el valor de los parámetros del
comando con los valores originales o
actuales de los campos de la fila basándose en la configuración del propio
comando. Cada parámetro de la colección Parameters de un comando tiene
la propiedad SourceColumn que indica
el nombre del campo cuyo valor deberá copiarse al parámetro, y la propiedad SourceVersion que indica si se trata
23
<< dnm.plataforma.ado.net
Y esta sería la instrucción UPDATE sin usar la
concurrencia optimista:
ciones, que sea capaz de gestionar los conflictos de
concurrencia y que disponga de todas las opciones
UPDATE Empleados
SET
DNI=@DNI, Nombre=@Nombre, Apellidos=@Apellidos
WHERE IdEmpleado=@Original_IdEmpleado
Como hemos dicho anteriormente, también
podemos usar un CommandBuilder. Este sería el
código a usar para nuestra tabla de ejemplo:
mencionadas en este artículo. Podéis encontrar un
DataAdapter para SQL Server (SqlRanger.SqlAdapter)
escrito por mí en la web de la revista o en mi propia
página web: http://sqlranger.com/descargas.aspx.
<<dotNetManía
SqlDataAdapter Adapter = new SqlDataAdapter(“SELECT * FROM Empleados”, Connection);
SqlCommandBuilder CommandBuilder = new SqlCommandBuilder(Adapter);
Adapter.UpdateCommand = CommandBuilder.GetUpdateCommand();
Adapter.InsertCommand = CommandBuilder.GetInsertCommand();
Adapter.DeleteCommand = CommandBuilder.GetDeleteCommand();
24
Las instrucciones UPDATE y DELETE serían
equivalentes a las generadas por el asistente usando concurrencia optimista y sin actualizar el conjunto de datos.
La alternativa de configurar manualmente el
DataAdapter no es muy recomendable, ya que
requiere escribir bastante código y la funcionalidad
obtenida es exactamente igual a la conseguida usando el asistente. Además es posible que cometamos
algún error al escribir el código, mientras que el
asistente no los comete.
Como vemos, el DataAdapter sólo nos deja la posibilidad de incluir en la cláusula WHERE de las instrucciones UPDATE y DELETE o bien la clave primaria, o bien todos los campos. No tenemos las otras
alternativas que se mencionan en este artículo. Además
en la cláusula SET de la instrucción UPDATE, sólo
podemos incluir todos los campos, no tenemos la
opción de incluir sólo los modificados.
Por otra parte, ADO.NET sólo da soporte para
la detección del conflicto de concurrencia, no hay
nada que nos ayude a gestionarlo, por lo que tendremos que escribir nosotros mismos el código
necesario.
El código de ejemplo del fuente 1 muestra como
gestionar conflictos de concurrencia, refrescando
el registro si ha sido modificado y eliminándolo si
ha sido eliminado.
Como vemos, gestionar los conflictos de concurrencia no es trivial y repetir el mismo código una y
otra vez para cada caso es muy laborioso y pesado.
Una buena alternativa a los DataAdapters que vienen incluidos en .NET Framework, es escribir nuestro propio DataAdapter que no tenga estas limita-
Este DataAdapter es completamente gratis y se
incluye el código fuente así como un ejemplo de su
uso. El SqlRanger.SqlAdapter tiene propiedades específicas para tratar la concurrencia. Entre las que se
incluyen:
• UpdateCriteria: Determina los campos a incluir
en la cláusula WHERE de la instrucción
UPDATE. Puede tomar los siguientes valores:
• All: Se incluirán los valores originales de
todos los campos.
• Key: Se incluirá sólo la clave primaria.
• Modified: Se incluirá la clave primaria más
los valores originales de los campos modificados.
• TimeStamp: Se incluirá la clave primaria más
el valor original del campo TimeStamp si es
que existe.
• UpdateColumns: Determina qué campos aparecerán en la cláusula SET de la instrucción
UPDATE. Puede tomar los siguientes valores:
• All: Se incluyen todos los campos.
• Modified: Se incluyen sólo los campos modificados.
• DeleteCriteria: Determina los campos a incluir
en la cláusula WHERE de la instrucción DELETE. Puede tomar los siguientes valores:
• All: Se incluirán los valores originales de
todos los campos.
• Key: Se incluirá sólo la clave primaria.
• TimeStamp: Se incluirá la clave primaria más
el valor original del campo TimeStamp si es
que existe.
<< dnm.plataforma.ado.net
public void Guardar(DataTable Empleados)
{
// creamos un adapter para realizar la actualización
SqlDataAdapter Adapter = new SqlDataAdapter(“SELECT * FROM Empleados”, this.cn);
// usamos un command builder para configurar los comandos de actualización
SqlCommandBuilder CommandBuilder = new SqlCommandBuilder(Adapter);
Adapter.UpdateCommand = CommandBuilder.GetUpdateCommand();
Adapter.InsertCommand = CommandBuilder.GetInsertCommand();
Adapter.DeleteCommand = CommandBuilder.GetDeleteCommand();
// este comando nos sirve para refrescar un registro
SqlCommand Resync = new SqlCommand(“SELECT * From Empleados WHERE IdEmpleado=@IdEmpleado”, this.cn);
Resync.Parameters.Add(“@IdEmpleado”, SqlDbType.Int);
try
{
Adapter.Update(Empleados);
}
catch ( DBConcurrencyException ex )
{
// Nuestra respuesta a un conflicto va a ser refrescar el registro
Adapter.SelectCommand = Resync;
Resync.Parameters[“@IdEmpleado”].Value = ex.Row[“IdEmpleado”, DataRowVersion.Original];
//
//
if
//
{
el método Fill buscará el registro en el DataTable
por clave primaria (IdEmpleado) y lo “refrescará”
( Adapter.Fill(Empleados) == 0 )
la causa del conflicto es que ha sido eliminado (Fill devuelve cero registros)
if ( ex.Row.RowState == DataRowState.Deleted )
{
// en este punto tenemos un conflicto de concurrencia
// al eliminar un registro porque ha sido eliminado.
// Eliminamos definitivamente el registro
ex.Row.AcceptChanges();
// ignoramos el conflicto y seguimos con la actualización
Guardar(Empleados);
}
else
{
// en este punto tenemos un conflicto de concurrencia
// al modificar un registro porque ha sido eliminado
// Eliminamos el registro
// y volvemos a lanzar la excepción
ex.Row.Delete();
ex.Row.AcceptChanges();
throw ex;
}
}
else
{
// la causa del conflicto es que ha sido modificado
// Si el conflicto ha sido al eliminar el registro
// Fill ya lo habrá “recuperado” y refrescado.
// Aparecerá el registro con el error
// Si el conflicto ha sido al modificar el registro
// Fill lo habrá “refrescado”. Y aparecerá el registro
// con el error
// sólo hay que volver a lanzar la excepción
throw ex;
}
Fuente 1. Ejemplo de gestión de conflictos de concurrencia
<<dotNetManía
}
}
25
<< dnm.plataforma.ado.net
• ConflictUpdatingChangedAction:
Determina la acción a realizar en
caso de un conflicto de concurrencia al actualizar un registro
porque haya sido modificado desde la última vez que se leyó. Puede
tomar los siguientes valores:
• NoAction: No hace nada.
• ResyncAllValues: Refresca todos
los valores del registro, volviéndolo a leer de la base de datos.
• ResyncOriginalValues: Refresca los
valores originales del registro,
leyéndolo de la base de datos.
• ConflictUpdatingDeletedAction:
Determina la acción a realizar en
caso de un conflicto de concurrencia producido al actualizar un registro porque haya sido eliminado desde la última vez que se leyó. Puede
tomar los siguientes valores:
¿Qué es qué?
¿Qué
• Delete: Elimina definitivamente el registro del DataSet.
• Insert: Vuelve a insertar el registro en la base de datos.
• NoAction: No hace nada.
• ConflictDeletingChangedAction:
Determina la acción a realizar en
caso de un conflicto de concurrencia producido al eliminar un
registro porque haya sido modificado desde la última vez que se
leyó. Puede tomar los siguientes
valores:
• NoAction: No hace nada.
• ResyncAllValues: Refresca el
registro, volviéndolo a leer de
la base de datos.
• ResyncCommand: Comando
parametrizado basado en clave primaria utilizado para
refrescar un registro.
Whitehorse es el nombre en clave del software que se incluirá en
Visual Studio 2005 y que aporta herramientas de diseño model-driven
dirigida a los arquitectos de software, enlazando el modelo conceptual
al código.
Tendremos más información en el devdays que se celebrará en San Diego,
California entre el 23 y el 28 de Mayo (http://www.microsoft.com/seminar/teched2004). En Europa se celebrará en Ámsterdam, Holanda, entre el
29 de Junio y el 2 de Julio (http://www.microsoft.com/europe/teched).
Entretanto puede descargarse un video demostrativo de la web de
MSDNTV en http://msdn.microsoft.com/msdntv
¿Qué es Laguna?
En el Microsoft Mobile DevCon Conference 2004 celebrado en
San Francisco entre el 23 y el 27 de marzo se habló de “Laguna”, nombre en clave del SQL Server CE 3.0. Esta versión se verá retrasada igual
que la versión completa, el SQL Server 2005. Según nuestras noticias,
ambas versiones saldrán juntas, si bien la versión beta 1 de Laguna estará disponible cuando esté la beta 2 de Yukon.
La web del Mobile DevCon Conference 2004 está en:
http://www.microsoftmdc.com.
Puede ver información de la versión actual de SQL Server CE 2.0
en: http://www.microsoft.com/sql/ce.
¿Qué es Indy?
<<dotNetManía
Conclusión
La concurrencia es un tema problemático en ADO.NET dada su naturaleza desconectada. Existen varias opciones para detectar y tratar los conflictos
de concurrencia. Cada una de estas
opciones tiene sus ventajas e inconvenientes y es necesario elegir cuidadosamente la más adecuada para el sistema
en cuestión. ADO.NET da soporte
limitado para la gestión de la concurrencia, siendo una buena alternativa
escribir nuestro propio DataAdapter para
superar las limitaciones.
es qué?
¿Qué es Whitehorse?
26
El SqlRanger.SqlAdapter genera automáticamente los comandos de actualización y el ResyncCommand, no siendo necesario proporcionárselos. Para ello hace
uso del SqlRanger.CommandBuilder.
Indy es el nombre en clave de una nueva herramienta de gestión
desarrollada por Microsoft Research y que se comercializará por la división Enterprise Management de Microsoft. Simula un centro de datos
empresarial derivado del modelo de hardware, software y los sistemas
de servidores del cliente.
Indy está inmerso en la versión 2.0 de la suite Microsoft System
Center para la que aún no hay fecha prevista de salida, ni tan siquiera
una aproximación. La versión actual, la 1.0 llamada System Center 2005
es la primera suite de gestión integrada para el Windows Server System
e incluye el System Management Server 2003, Microsoft Operations
Manager 2005 y el nuevo sistema común de reporting.
Se habló de él en el Summit celebrado en Las Vegas el pasado mes de
marzo. La web del Summit 2004: http://www2.mms2004.com.
Más información en Microsoft Watch: http://www.microsoft-watch.com
y en el sito Betanews http://www.betanews.com/article.php3?sid=1079576470
¿Qué es Lonestar?
Aparte de un mítico grupo de rock catalán de los años 70, Lonestar es
el nombre en clave de la próxima versión del sistema operativo de Microsoft
para Tablet PC. Si bien se iba a vender como un add-on para los usuarios
de Tablet PC, finalmente será incluido dentro de Windows XP SP2. Tendrá
un nuevo SDK para desarrolladores e integración con Office 2003.
¿Qué es Windows XP Reloaded?
Windows XP Reloaded es el nombre en clave para la versión de
Windows XP que hará de puente entre la actual y Longhorn.
¿Qué es Symphony y Harmony?
Symphony es el nombre en clave de la próxima versión de Windows
XP Media Center Edition el cual está basado en XP SP2. Una versión
previa a Windows XP Media Center Edition 2004. Harmony es el nombre en clave del próximo Windows XP Media Center Edition 2004.
Incluirá soporte para High Definition Televisión, soporte para múltiples sintonizadores, soporte para diferentes formatos de grabación de
vídeo y radio.
noticias.noticias.noticias.noticias
Suscripción a dotNetManía
❑ Deseo suscribirme a dotNetManía por un año (11 ejemplares) y beneficiarme de la oferta del 10% de descuento por un
importe total de 60 € para España; o por 75 € para el resto de Europa; o por 90 € para el resto del mundo (IVA incluido).
❑ Deseo suscribirme a dotNetManía por un año (11 ejemplares) por un importe de 45 € por ser estudiante (IVA incluido).
Aporto fotocopia del carné de estudiante o sello del centro académico (IMPRESCINDIBLE). OFERTA VÁLIDA SÓLO
PARA ESTUDIANTES RESIDENTES EN ESPAÑA.
IMPORTES VÁLIDOS HASTA NUEVA OFERTA
DATOS DE FACTURACIÓN
CIF/NIF . . . . . . . . . . . . . . . . . . . . .Empresa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Nombre y apellidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Dirección . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Población . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .Código Postal . . . . . . . . . . . . . . . . . . . Provincia . . . . . . . . . . . . . . . . . . . . . . . . .
Teléfono . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fax . . . . . . . . . . . . . . . . . . . . . . . . . . . email . . . . . . . . . . . . . . . . . . . . . . . . . . . .
DATOS DE ENVÍO (sólo si son distintos de los datos de facturación)
CIF/NIF . . . . . . . . . . . . . . . . . . . . .Empresa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Nombre y apellidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Dirección . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Población . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .Código Postal . . . . . . . . . . . . . . . . . . . Provincia . . . . . . . . . . . . . . . . . . . . . . . . .
Teléfono . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fax . . . . . . . . . . . . . . . . . . . . . . . . . . . email . . . . . . . . . . . . . . . . . . . . . . . . . . . .
FORMA DE PAGO
❑ Talón nominativo a nombre NETALIA, S.L.
❑ Transferencia bancaria a nombre de NETALIA, S.L. a:
La Caixa - Número de cuenta 2100 4315 48 2200014696 (Indique su nombre en la transferencia)
❑ Domiciliación Bancaria (con renovación automática, previo aviso)
Indique su número de cuenta:
❑ Tarjeta de crédito
❑ VISA
❑ MASTERCARD
Número de su tarjeta:
Fecha de caducidad:
/
(imprescindible)
Firma y/o sello (imprescindible)
a
❑ Nº3
❑ Nº4
❑ Nº5
de
❑ Nº6
Usted autoriza a la mecanización
de estos datos. El responsable y
destinatario de éstos es Netalia,
S.L. Usted tiene derecho a acceder
a sus datos, modificarlos y cancelarlos cuando lo desee. Sus datos
no serán cedidos en ninguna de las
formas posibles a terceras partes y
no se utilizarán más que para el
buen funcionamiento de su suscripción a la revista dotNetManía
y para informarle de las actividades comerciales que realice la editorial Netalia, S.L. Si no desea recibir información comercial de
dotNetManía marque la casilla
siguiente ❑
de 20
❑ Nº7
❑ Nº8
Si desea algún otro número indíquelo
Puede enviar los datos al email [email protected],
al FAX (34) 91 499 13 64 o al teléfono (34) 91 666 74 77.
También puede enviarlo por correo postal a la siguiente dirección:
C/ Robledal, 135
28529- Rivas Vaciamadrid
Madrid (España)
❑ Nº9
Descargar