Esta mañana necesitaba ordenar los elementos de una tabla en función de uno de los campos, pero el problema era que la comparación no era alfabética ni similar, era en función de unos criterios propios; uno de los parámetros que aceptan los métodos extensores OrderBy, OrderByDescending, ThenBy and ThenByDescending es un IComparer<>, probé y funcionó, asignaba un peso a cada valor y luego comparaba en el método Compare los pesos de los dos parámetros de entrada, pero aún asi no me gustaba como quedaba, no me gustaba lo de tener que escribir una clase solo para este propósito... buscaba algo más... compacto... asi que con ayuda de mi compañero use un delegado anónimo primero que directamente devolvia el peso en TKey inferido como Int32, que es en lo que LINQ basaria la ordenación y luego lo substituí por una expresión lambda; es realmente interesante como LINQ te facilita la vida :D
Este pequeño ejemplo ordena un DataTable en función de su columna "Country" asignando a cada valor un peso en Int32 y siendo este peso el que devuelve la expresión infiriendo TKey como Int32, que será en lo que LINQ base ordenación de los elementos. Anotar que el elemento que queramos que tenga más relevancia debe ser el de menos peso y el que menos el que más peso:
DataTable EmployeesByCountry =
EmployeesTable.AsEnumerable()
//.OrderBy(delegate(DataRow Employee)
.OrderBy(Employee =>
{ switch(Employee.Field<String>("Country")) { case "Denmark":return 3;
case "France":return 2;
case "Ireland":return 4;
case "Spain":return 1;
case "UK":return 5;
default: return Int32.MaxValue;
}
}).CopyToDataTable();
Aún así estoy seguro que habrá formas mejores y más sencillas de hacerlo. Evidentemente es mejor si tenemos un algorritmo que calcule el peso en lugar de tener que fijarlos "a capón" ... pero hay situaciones... que no hay más remedio :D
Últimamente ya no escribo nada, estoy totalmente inmerso en el mundo LINQ con el C# 3.0 y LINQ de Octavio Hernandez y el LINQ en Action ... y poco hay que contar que no este en libros ó en los cientos de blogs que llevan escribiendo sobre LINQ desde hace mucho tiempo. Aún asi por lo menos iré escribiendo alguna cosilla sobre LINQ aunque solo sea para que no se me olvide como hacer ciertas cosas y tener que volverlas a buscar :P
Este es un ejemplo simple de como lanzar una consulta Update-Select con LINQ sobre un DataTable, apoyandonos en el ya famoso método extensor ForEach ... (que según me ha comentado un compañero es posible que aparezca en la BCL próximamente), lo que hace es en cada DataRow que cumple con las condiciones dadas introduce en la columna "Initial" la primera letra del nombre... (nada complicado vamos xD)
EmployeesTable.AsEnumerable()
.Where(EmployeeRow => !EmployeeRow.IsNull("Country") && EmployeeRow.Field<String>("Country") == "UK" && !EmployeeRow.IsNull("FirstName") && EmployeeRow.Field<String>("FirstName") != String.Empty) .ForEach(EmployeeRow => EmployeeRow.SetField<String>("Initial", EmployeeRow.Field<String>("FirstName").Substring(0, 1)));
También empezaré ahora a escribir sobre WPF... que me toca ponerme las pilas :D
Mas sencillo de lo que imaginaba, se trata simplemente de definir el siguiente delegado System.Net.ServicePointManager.ServerCertificateValidationCallbackcon la funcion que se encarga de validar el certificado.
Por ejemplo:
System.Net.ServicePointManager.ServerCertificateValidationCallback =
new System.Net.Security.RemoteCertificateValidationCallback(ValidateSSL);
Y escribimos el metodo:
static Boolean ValidateSSL(object sender, X509Certificate certificate,
X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
// Validar el certificado...
// Un ejemplo tosco para aceptar cualquiera sin errores.
return sslPolicyErrors == SslPolicyErrors.None;
}
Una aplicación modular suele ser una aplicación donde sus funcionalidades son opcionales, de forma que podemos quitar ó añadirlas según nos convenga. La aplicación solo sabe que va a tratar con instancias que cumplen un determinado contrato, ya sea cumpliendo con una interfaz ó determinado tipo base (usando clases abstractas). Estos contratos suelen estar en ensamblados que conocen las dos partes, de forma que la aplicación espera una instancia de clase que cumple el contrato definido en el ensamblado común, y el ensamblado "opcional" provee una instancia de clase que cumple dicho contrato, dicho contrato ó acuerdo mutuo indica a la aplicación como usar dicha instancia. En el ejemplo de solución de Visual Studio que se ve a la izquierda, LoadTypeTest sería la aplicación, Common el ensamblado compartido donde se definen los contratos y los tipos comunes, y LibraryTest donde se encuentra definida la clase, LoadTypeTest y LibraryTest tienen referenciado a Common, pero se desconocen entre ellos.
En en ensamblado común he definido una interfaz y un delegado, que definen la forma en que la instancia "desconocida" se va a relacionar con la aplicación:
public delegate void DataArrival_(String Data);
public interface ITestInterface
{ String Name { get; }
event DataArrival_ DataArrival;
void SendData(String Data);
}
Si la clase que que implementamos cumpliendo esta interfaz, tiene un constructor por defecto, tenemos dos formas de cargar el tipo dinámicamente:
Reflection:
Assembly myAssembly = Assembly.LoadFrom("LibraryTest.dll");ITestInterface test2 = myAssembly.CreateInstance("LibraryTest.LibraryTest") as ITestInterface;test2.DataArrival += new DataArrival_(delegate(String Data)
{ Console.WriteLine("Received: " + Data);});
test2.SendData("Hello " + test2.Name + "!!");
Remoting:
ObjectHandle obj = Activator.CreateInstance("LibraryTest", "LibraryTest.LibraryTest");ITestInterface test = obj.Unwrap() as ITestInterface;
test.DataArrival += new DataArrival_(delegate(String Data)
{ Console.WriteLine("Received: " + Data); });
test.SendData("Hello "+ test.Name +"!!");
En este contexto, nos valen ambos planteamientos por igual, ambos cargan la instancia en el mismo dominio de aplicación que la aplicación principal y su rendimiento es bastante similar, a excepción de la primera instancia que realiza Remoting, que parece que le cuesta un poco más:
Pero, cuando la instancia tiene un constructor parametrizado tendrémos que usar una mezcla de las dos, ya que para poder invocar un constructor específico debemos de proveer el tipo a Activator.CreateInstance, que por supuesto lo podemos extraer con Reflection; vamos a suponer que la calse "desconocida" espera dos parámetros, un Int32 y un DataTable (ojo con el orden de los parámetros):
Assembly myAssembly = Assembly.LoadFrom("LibraryTest.dll");Type myType = myAssembly.GetType("LibraryTest.LibraryTest");ITestInterface test = Activator.CreateInstance(myType,myIndex,myTable) as ITestInterface;
test.DataArrival += new DataArrival_(delegate(String Data)
{ Console.WriteLine("Received: " + Data); });
test.SendData("Hello " + test.Name + "!!");
Pero... y si ni siquiera supiesemos que número de parámetros tiene el constructor... o que tipo de parámetros son... pues con Reflection podemos interrogar al tipo para que nos de información sobre los constructores que hay en la clase y sus respectivos parámetros. Un ejemplo un poco tosco :
Assembly myAssembly = Assembly.LoadFrom("LibraryTest.dll");Type myType = myAssembly.GetType("LibraryTest.LibraryTest");
// Interrogo al tipo para comprobar si existe el constructor
// que necesito.
Object[] constructorParameters = null;
foreach (ConstructorInfo constructor in myType.GetConstructors())
{ ParameterInfo[] parameters = constructor.GetParameters();
if ((parameters[0].ParameterType == typeof(Int32)) &&
parameters[1].ParameterType == typeof(DataTable))
{ constructorParameters = new Object[] {myIndex, myTable }; }
parameters.ToString();
}
ITestInterface test = Activator.CreateInstance(myType,constructorParameters) as ITestInterface;
test.DataArrival += new DataArrival_(delegate(String Data)
{ Console.WriteLine("Received: " + Data); });
test.SendData("Hello " + test.Name + "!!");
Como decía este es un ejemplo un poco tosco, podemos mejorar la lógica para detectar el constructor, ordenar los parámetros... etc.. etc..
Lo máximo que sabe la aplicación sobre la clase, es su nombre y en que ensamblado esta, cosas que le podemos pasar como parámetros ó tenerlo alojado en un archivo de configuración, de forma que alterando dicho archivo la aplicación usa una u otras funcionalidades. Y con esto e imaginación puedes hacer tus arquitecturas tan inteligentes y escalables como tu quieras, de forma que sean capaces de cargar tipos sin conocerlos previamente y usarlos por medio de una interfaz ó clase abstracta. Si a esto le añades el uso de atributos como metadatos para describir las clases... aún puedes conseguir cosas más inteligentes y escalables, a ver si tengo tiempo otro día para escribir sobre este tema, que es bastante apasionante :)
Realizar casting entre clases es siempre sencillo, el inconveniente es cuando se trata de un arrays. Por ejemplo... sabiendo que la clase Manager deriva de Employee, si tenemos un array de Manager, es fácil convertirlo en un array de Employee usando casting directo:
Employee[] employees1 = (Employee[])managers;
Pero si tenemos un array de Employee no es posible convertirlo en un array de Manager de la misma forma que tampoco podríamos hacer un casting directo entre objetos simples. No se me ocurre razón cuerda para querer hacer tal cosa, pero otro ejemplo más cotidiano puede ser cuando intentamos mostrar el contenido de un array en una línea usando String.Join, nos daremos cuenta que tiene que ser String[]. Existen muchas situaciones donde no hay un casting directo.
Afortunadamente disponemos del método genérico Array.ConvertAll<,> y del delegado genérico Converter<,> que nos pueden ayudar a simplificar estas cosas:
// Primero definimos como se convierte una clase en la otra
Converter<Employee, String> Employee2String = delegate(Employee m) { return m.Name; };
// Después convertimos todos los elementos del array usando ese delgado
String[] sArray = Array.ConvertAll<Employee,String>(Employees,Employee2String);
Ó podemos hacerlo en una sola línea aprovechando la inferencia de tipos:
String[] sArray = Array.ConvertAll<Employee,String>(Employees,delegate(Employee m) { return m.Name; });
La interfaz IDisposable nos provee del método .Dispose() que utilizamos para liberar los recursos que esta usando ese objeto, pero dicho método... no deja de ser un simple método ;) Solo hace falta hacer una pequeña prueba para darse cuenta:
1: class Program
2: { 3: static void Main(string[] args)
4: { 5: Desechable test = new Desechable();
6:
7: test.Dispose();
8:
9: Console.WriteLine(test.Cadena??"Muerto"); // Muestra: Vivo!
10: Console.ReadKey(true);
11: }
12: }
13:
14: class Desechable : IDisposable
15: { 16: public String Cadena = "Vivo!";
17:
18: public void Dispose()
19: { 20:
21: }
22: }
Ok ok... un poco más complejo:
1: class Program
2: { 3: static void Main(string[] args)
4: { 5: Desechable test = new Desechable();
6: WeakReference wr = new WeakReference(test.Tabla.Columns[0]);
7:
8: test.Dispose();
9: test = null;
10:
11: Console.WriteLine(((DataColumn)wr.Target).ColumnName); // Muestra "Columna"
12: Console.ReadKey(true);
13: }
14: }
15:
16: class Desechable : IDisposable
17: { 18: public DataTable Tabla = new DataTable();
19:
20: public Desechable()
21: { 22: Tabla.Columns.Add(new DataColumn("Columna")); 23: }
24:
25: public void Dispose()
26: { 27: Tabla.Dispose();
28: }
29: }
El resultado cambia si en la línea 10 hacemos un GC.Collect() ;D
Como vemos, el único que puede liberar memoria en el .NET Framework es el Garbage Collector, por lo que el método .Dispose() no libera memoria in libera nada, solo nos sirve para asegurar que el objeto que estamos desechando ha cerrado correctamente todos sus recursos y podemos olvidarnos de él, ya se encargará el GC de liberar la memoria cuando lo crea necesario.
Entonces, implementar la interfaz IDisposable no hace nuestros objetos "destruibles" bajo demanda, ni setear todos los campos a null en Dispose no va a hacer que sea recolectado más deprisa, ni no hacerlo va a evitar que sea recolectado, ni debemos des-subscribir los eventos...
Por lo tanto, debemos implementar el patrón IDisposable en una clase siempre que:
- Nuestra clase deriva de una clase que lo implementa.
- Nuestra clase esta compuesta de otras clases que lo implementan.
- Hagamos uso de recursos no administrados.
Para todo lo demás... mastercard confia en el GC :D
Un tema que causa controversia en este aspecto es... ¿que pasa con los delegados? ¿Como el estar subscrito a delegados afecta a la recolección de memoria? Bien, el estar subscrito a un delegado de una clase, no afecta en su recolección:
1: class Program
2: { 3: static void Main(string[] args)
4: { 5: MiClase test = new MiClase();
6: WeakReference wr = new WeakReference(test);
7:
8: test.MiEvento += new EventHandler(test_MiEvento);
9:
10: test = null;
11: GC.Collect();
12:
13: Console.WriteLine(wr.IsAlive ? "Vivo" : "Muerto"); // Muestra "Muerto"
14: Console.ReadKey(true);
15: }
16:
17: static void test_MiEvento(object sender, EventArgs e)
18: { 19: throw new NotImplementedException();
20: }
21: }
22:
23: class MiClase
24: { 25: public event EventHandler MiEvento;
26: }
Pero al revés, es decir, que una clase este subscrita a uno de nuestros delegados... si provoca que el objeto no pueda ser recolectado:
1: class Program
2: { 3: static event EventHandler Evento;
4: static void Main(string[] args)
5: { 6: MiClase test = new MiClase();
7: WeakReference wr = new WeakReference(test);
8:
9: Evento += test.Manejador_Evento;
10:
11: test = null;
12: GC.Collect();
13:
14: Console.WriteLine(wr.IsAlive ? "Vivo" : "Muerto"); // Muestra "Vivo"
15: Console.WriteLine(Evento.GetInvocationList().Length); // Muestra 1
16: Console.ReadKey(true);
17: }
18: }
19:
20: class MiClase
21: { 22: public void Manejador_Evento(object sender, EventArgs e)
23: { 24: throw new NotImplementedException();
25: }
26: }
Y ojo, que los delegados son muy comodos para ejecutar muchos métodos de una pasada (por ejemplo podríamos tener una serie de objetos subscritos a un delegado y cada vez que invocaramos a este se ejecutaria ese método en todos los objetos), pero como veis pueden causar un memory leak; aunque en ese caso poco podemos hacer desde el método Dispose ya que es otra clase la que ha de desuscribirse.
Saludos desde el frio Dublin donde el Verano es una broma de mal gusto.
La inicialización rápida de objetos es una nueva e interesante característica de C# 3.0, pero me gustaria reflexionar un poco sobre su uso y el de nuestros amigos los constructores.
Un constructor es el método de una instancia que implementa las acciones necesarias para inicializar la instancia de la clase, y es invocado por el operador new cuando instanciamos dicha clase. Por lo tanto, cuando desarrollamos una clase, el ó los constructores deben de tener la capacidad de inicializar los aspectos básicos de funcionamiento del objeto.
En C#, cuando no implementamos ningún constructor, el compilador asume que tenemos un constructor sin parámetros, es decir, esto:
[more]
class MyClass
{ public String Field1 { get; private set; } public String Field2 { get; private set; }}
es lo mismo que esto:
class MyClass
{ public String Field1 { get; private set; } public String Field2 { get; private set; }
public MyClass()
{ }
}
En el momento que definamos un constructor con parámetros, el compilador deja de asumir la existencia del constructor sin ellos, el motivo me parece más que obvio, si definimos un constructor con parámetros estamos diciendo que nuestra clase necesita "otras cosas" para poder inicializarse correctamente; si queremos tener otro sin parámetros tendremos que definirlo explicitamente.
Una ve las ventajas de usar un constructor paramétrizado, es por ejemplo inicializar propiedades públicas con setter privado, y modificarlas desde el constructor en base a los parámetros (seguro que no lo has hecho nunca xD); pero la principal ventaja es que le dice al programador que se necesita para ser inicializado y poder empezar a trabajar. A veces no necesitamos parámetros y otras veces es inevitable y debemos definir un constructor sin parámetros, como por ejemplo para permitir la serialización en Xml.
Definir constructores es bastante llevadero y poco pesado, ya que gracias a la sobrecarga, se escriben rápido, sin repetir el código en cada uno de ellos y queda bastante claro:
class MyControl : Control
{ public String Field1 { get; private set; } public String Field2 { get; private set; } public String Field3 { get; private set; }
// base: nos permite pasar parámetros a la
// clase de la que heredamos
public MyControl(Control parent, String text, String field1)
: base(parent, text)
{ this.Field1 = field1;
}
// this: nos permite invocar a otro constructor
// de la misma clase, de forma que lo que escribimos
// en este prosigue la labor del otro, evitandonos escribir
// otra vez lo mismo para cada constructor.
public MyControl(Control parent, String text, String field1, String field2)
: this(parent, text, field1)
{ this.Field2 = field2;
}
// this: nos permite llamar a cualquier constructor
// de la misma clase.
public MyControl(Control parent, String text, String field1, String field2, String field3)
: this(parent, text, field1,field2)
{ this.Field3 = field3;
}
}
También se pueden definir modificadores de acceso en los constructores, de forma que haya constructores que solo se puedan usar desde dentro de la misma clase(private), clase derivada(protected), ensamblado(internal) ó por todo el mundo(public). Útil para controlar desde donde se pueden instanciar nuestros objetos, ó cuantas veces, como en el patrón Singleton.
Además, podemos definir un constructor estático, sin parámetros, sin modificadores de acceso, imposible de invocar directamente... pero que nos permite inicializar lo que queramos antes que nigún miembro de la clase, estático ó de instancia, sea usado.
Bien, en C# 3.0 la inicialización rápida de objetos, permiten inicializar las propiedades públicas de una clase de una pasada:
class MyControl : Control
{ public String Field1 { get; set; } public String Field2 { get; set; } public String Field3 { get; set; } public String Field4 { get; private set; }}
static void Main()
{ MyControl mc = new MyControl() { Field1 = "f1", Field2 = "f2", Field3 = "f3" };}
No voy a discutir de lo útil que es, porque definitivamente lo es, pero por supuesto no es una caracteristica para reemplazar los constructores porque:
- No podemos inicializar campos privados ó propiedades públicas con setter privado.
- Puede que necesitamos ejecutar alguna lógica ó incluso otros métodos en función del tipo de parámetros.
- No denota cuales son las propiedades que necesita tener configuradas para poder considerarse totalmente inicializado, de cara a alguien que consuma nuestra clase a posteriori. Esto me parece lo más importante, ya que el código debe intentar ser autodescriptivo.
Al igual que los métodos parciales, métodos extensores ó tipos anónimos, es una nueva caracteristica para tener en cuenta y para usar cuando sea apropiado, no viene a reemplazar nada, viene a complementar para "esos casos" donde hace falta.
Si tu clase necesita datos ó ser inicializada, implementa un constructor parametrizado. Por la salud mental de nuestros compañeros de trabajo, hagamos un buen uso del lenguaje :D
Pedazo de maquinón que estrené en el trabajo y con el que estoy más que contento, es un placer trabajar así.
Mi jefe es de esas personas que sabe como hacerte feliz. Después de algo más de una semana trabajando con él, cuando cojo mi portátil ya no es lo mismo... definitivamente tengo que hacerme con uno de estos para mi casa :D
El monitor no es que se quede atrás:
Si trabajas con p/invoke tanto habitual como eventualmente y pinvoke.net esta en tus bookmarks... esta herramienta no te va a dejar indiferente :D Leo en el blog del BCL Team:
El equipo Interop ha publicado recientemente una nueva herramienta llamada P/Invoke Interop Assistant. Esta herramienta genera automáticamente las declaraciones en C# ó VB.NET de las llamadas p/invoke nativas. Incluye una búsqueda rápida por las librerias comunes Win32 y permite generar las declaraciones para cualquier otra libreria nativa simplemente pasando la firma nativa. Esto hace mucho más facil trabajar con interop correctamente, sin tener que comprender todas las reglas y atributos usados cuando "conecta" el desarrollo administrado y no administrado. `
La herramienta fue publicada en MSDN Magazine como parte del artículo Marshaling between Managed and Unmanaged Code en Enero. Ahora esta en CodePlex junto al código fuente. Altamente recomendado echarle un ojo!!
Impresionante!! La estoy usando y me va a ahorrar mucho tiempo en búsquedas :D Obviamente, no va a substituir a pinvoke.net porque dicha web tiene algo muy importante, ejemplos y tips (que habría sido de mi sin esta web estas dos semanas...), mientras que este programa solo te proporciona las firmas... pero esta claro que es una herramienta indispensable si se trabaja con interop :)
Estoy disfrutando de lo que será mi último proyecto en C# 2.0 y .NET 2.0 antes de pasar a WPF, y la verdad es que se hace amargo algunas veces cuando ya conoces C# 3.0, por ejemplo se hecha de menos LINQ :D
En este artículo quiero demostrar como hacer queries a una colección de elementos en forma de arbol desde C# 2.0, de forma que se pueda definir el tipo de colección, criterio de búsqueda que se quiere usar y obtener los resultado conforme se vayan obteniendo.
Una función recursiva, es una función que se llama a si misma y controla cuando parar de hacerlo mediante una condición. Si esa condición no esta bien definida... a parte del cuelgue del hilo en cuestión lo más posible será terminar recibiendo una sonora StackOverflowException.
Para recorrer una estructura en arbol nada mejor que una función recursiva, un ejemplo muy simple es esta función que recorre todos los controles hijos de un control dado recursivamente:
/// <summary>
/// Recorre recursivamente todos los
/// controles hijos de un control dado.
/// </summary>
void lookRecursive(Control Parent)
{ // Recorro los subnodos
foreach (Control child in Parent.Controls)
{ Debug.Print(child.Name);
// Si el subnodo tiene más subnodos
// ejecuto recursivamente.
if (child.Controls.Count > 0)
lookRecursive(child);
}
}
Podemos hacer que la función nos devuelva un elemento introduciendo una variable para el almacenamiento, si por ejemplo lo que queremos es que encuentre un determinado control por su nombre y lo devuelva (ya hay un método por ahí por la BCL que lo hace):
/// <summary>
/// Recorre recursivamente todos los
/// controles y encuentra el de un nombre
/// determinado.
/// </summary>
Control findControl(Control Parent, String Name)
{ Control temp=null;
// Recorro los subnodos
foreach (Control child in Parent.Controls)
{ // Criterio de búsqueda
if (child.Name == Name)
temp = child;
else if((temp==null) && (child.Controls.Count > 0))
temp = findControl(child,Name);
}
return temp;
}
Ahora vamos a complicarlo un poco más añadiendo las siguientes características:
- Adaptable a cualquier tipo de objetos: Usando genéricos.
- Definir nuestro propio criterio de búsqueda: Pasando un Predicate como argumento que contenga el criterio.
- Definir como se obtienen los sub-nodos: Pasando un delegado que lo especifique.
- La posibilidad de que el resultado no sea único: Devolviendo una colección ó array.
- Poder parar de recorrer cuando encontremos lo que queremos: Que la colección devuelta sea IEnumerable ó IEnumerable<T>y devolver con yield.
Asi queda:
delegate IEnumerable getChilds<T>(T Element);
IEnumerable<T> findRecursive<T>(T Parent, getChilds<T> Elements, Predicate<T> Criteria)
{ foreach (T element in Elements.Invoke(Parent))
{ if (Criteria.Invoke(element))
yield return element;
foreach (T element2 in findRecursive<T>(element,Elements, Criteria))
yield return element2;
}
}
El delegado getChilds define como obtener subnodos de un nodo dado.
Nota: En este ejemplo, en el delegado devuelvo IEnumerable y no IEnumberable<T> (que seria lo lógico) porque Control.Controls es un ControlCollection que solo implementa dicha interfaz, no la genérica :P
Para usarlo... por ejemplo, queremos que todos los paneles pasen a tener borde FixedSingle:
// Defino el criterio de búsqueda,
// en este caso, quiero los controles
// que sean paneles.
Predicate<Control> lookForPanels = delegate(Control c)
{ return c is Panel;
};
// Defino como obtengo los hijos de
// este tipo de objeto.
getChilds<Control> getChildControls = delegate(Control C)
{ return C.Controls;
};
// Por último efectuo la búsqueda
// y obtengo los resultados.
foreach (Panel c in findRecursive<Control>(this, getChildControls, lookForPanels))
{ c.BorderStyle = BorderStyle.FixedSingle;
}
Y si se quiere más complejo también se puede recorrer una estructura en arbol con múltiples hilos:P
Bueno pues las vacaciones se acaban y es la hora de volver a trabajar y a .NET :D El próximo día 23 me incorporo a Datakraft en Dublín como desarrollador y debo confesar que estoy bastante excitado con el tema, voy a tener la oportunidad de sumergirme en .NET 3.5, desarrollo Windows/web/back-end y metodologías ágiles.
Con esto completo mi instalación en Dublín al 100%, después de 2 meses y medio aprendiendo inglés en el LCI, encontrar casa y encontrar trabajo. Otro día contaré como ha sido el proceso a grandes rasgos, pero desde luego ha sido una aventura desde el primer día y sin lugar a dudas considero que fué una buena decisión, no solo por aprender inglés, si no por conocer gente de todo el mundo y la aventura del día a día defendiendote con lo poco que hablas y lo menos que entiendes. Ahora no hablo un inglés para tirar cohetes, pero suficiente para defenderme en el día a día (pero aún no entiendo Los Simpson en inglés xD), espero que en un par de años hablar y entender sin ningún tipo de problemas.
Desde aquí me gustaria agradecer a mis antiguas empresas Avanade y Experian por prestarse a dar referencias a mi nueva empresa sobre mi y a mi ex-compañero de Avanade Gerardo K. por prestarse a escribir una referencia técnica recomendándome.
Ahora una semana para España... que echo de menos muchas cosas... :)
Hora de volver a trabajar... asi que toca quitarle el polvo al Visual Studio que tenia abandonado ya estos meses, la buena vida se acaba... xD
En determinadas ocasiones, necesitamos lanzar un cierto número de ejecuciones en paralelo y esperar a que terminen todas para devolver el resultado final. Existen diversas formas de hacerlo, y yo voy a poner la que, después de darle un par de vueltas... me parece la mejor y más sencilla, ... se puede hacer aún mejor, pero no se si más sencilla... y no se si lo que se puede mejorar merece la pena en cuanto a la complejidad que añade.
Un ejemplo práctico, este método pertenece a una clase que estoy programando para realizar pruebas de estres y carga, la clase tiene una lista de clientes y cuando se ejecuta esta función ejecuta el delegado que se pasa como argumento en paralelo en todos los clientes a la vez, recolecta las excepciones capturadas y las devuelve como una lista. Por supuesto, tiene que esperar a que terminen todas las pruebas en paralelo para poder devolver la lista :P
private List<Exception> test(Int32 Times, Int32 Interval, TestDlg_ Test)
{ List<Exception> errors = new List<Exception>();
List<IAsyncResult> working = new List<IAsyncResult>();
lock (working) // Bloqueo hasta que lanze todos los clientes
{ foreach (IImApplicationClient client in this.Clients)
{ working.Add(Test.BeginInvoke(client, Times, Interval,
delegate(IAsyncResult IA)
{ try
{ Test.EndInvoke(IA);
lock (working)
{ working.Remove(IA); // Lo elimino de la lista,
if (working.Count == 0) // y si era el último...
Monitor.Pulse(working); // envio pulso para liberar
} // la espera
}
catch (Exception ex)
{ IImApplicationClient asynClient =
IA.AsyncState as IImApplicationClient;
lock(errors)
errors.Add(new Exception
(client != null ? asynClient.Id : "Id unknown.", ex));
}
}, client));
}
if (working.Count > 0) // Si se han lanzado clientes...
Monitor.Wait(working); // bloqueo en espera.
}
return errors;
}
El planteamiento es el siguiente:
- Se usa una lista para tener controladas las llamadas asíncronas.
- Cada vez que se lanza una llamada asíncrona, el IAsyncResult resultante se añade a la lista.
- Se bloquea la lista hasta que todas han sido lanzadas, y si se ha lanzado más de una, se ejecuta un Monitor.Wait para que se detenga la ejecución ahí hasta que se realize un Monitor.Pulse.
- Cada vez que una llamada asíncrona termina, se elimina su IAsyncResult de la lista.
- Si el IAsyncResult es el último que quedaba, se realiza un Monitor.Pulse para que la ejecución pueda seguir puesto que ya finalizaron todas las llamadas asíncronas.
Sugerencias, como siempre, bienvenidas :D
Hoy Second Nug tiene el honor de presentar a dos ponentes de excepción, Guillermo Som (el Guille) y Marino Posadas (el Ave Fénix), en la que será una gran batalla donde dos voces con gran experiencia expondrán sus mejores bazas en un duelo sin igual: VB .NET vs C# .NET.
Sabremos por qué se decantaron por un lenguaje u otro y nos presentarán su evolución en las nuevas versiones del .NET Framework. Nos desvelaran secretos y trucos de sus defendidos y veremos si es cierto aquello de que lo que se puede hacer con uno, se puede realmente hacer con el otro.
Además los asistentes podrán resolver sus dudas preguntando a nuestros expertos y participar en el resultado final de la contienda.
El eterno dilema al descubierto. ¿Quién será el vencedor?
El evento será a las 19:30 - 21:30 (GMT+2), y como en las anteriores ocasiones, se retransmitirá vía Web a través de Live Meeting.
Si no tienes Live Meeting, puedes descargarlo en el siguiente enlace.
Podéis registraros en el evento en el siguiente enlace.
Manejar el registro de Windows es algo... en parte tedioso por lo extenso y medianamente documentado que esta lo que contiene. En mi humilde opinión, es algo a evitar siempre que sea posible en virtud de los archivos de configuración; pero en ocasiones necesitamos crear/modificar entradas que afectan al funcionamiento de otros programas ó del propio Windows desde nuestras aplicaciones, asi que hay que arremangarse y meterle mano :D
Primero un pequeño repaso de como se estructura la información a grandes rasgos. Podemos ver el registro mediante la aplicación regedit.exe. Encontramos 5 elementos principales, que así por encima vienen a ser:
- HKEY_CLASSES_ROOT (HKCR) : Contiene las realciones entre los identificadores de programa y clases que se usan por COM+, asociaciones de archivo e información de ejecución de aplicaciones.
- HKEY_USERS (HKU) : Contiene un elemento por cada usuario del sistema y en cada uno se almacenan sus preferencias y configuraciones individuales.
- HKEY_CURRENT_USER (HKCU) : Es como un enlace simbólico a la entrada en HKU correspondiente al usuario actual.
- HKEY_LOCAL_MACHINE (HKLM) : Información del sistema y del hardware.
- HKEY_CURRENT_CONFIG (HKCC) : Es como un enlace simbólico a la entrada en HKLM correspondiente a la configuración hardware actual.
La información esta estructurada en forma de arbol mediante claves y valores. Las claves son como carpetas que puede contener otras claves, y los valores son una pareja key-value donde podemos asignar un valor. Los valores pueden ser de tipo:
- REG_DWORD : Número de 4 bytes, se usa para almacenar valores boleanos.
- REG_BINARY : Almacena datos binarios.
- REG_SZ : Cadena de texto.
- REG_MULTI_SZ : Almacena arrays de strings.
- REG_EXPAND_SZ : Almacena una variable en formato de string.
Ahora vamos a meterle mano, lo primero que necesitamos es el namespace Microsoft.Win32 , y nuestras clases para trabajar serán Registry y RegistryKey. Lo primero, es llegar donde vamos a trabajar:
// Abrimos la clave del registro con la que queremos trabajar
RegistryKey rk1 = Registry.LocalMachine;
Trabajando con claves:
// Nos movemos hasta la subclave donde queremos trabajar.
// El parámetro boleano indica si la abrimos en solo lectura (false)
// ó en lectura/escritura (true).
rk1 = rk1.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon",true);
// Si devuelve null es que la clave no existe
if (rk1 == null)
Console.WriteLine("No existe esa clave");
// Crear una nueva clave
// El método devuelve un RegistryKey apuntando
// a la nueva entrada.
RegistryKey rk2 = rk1.CreateSubKey("Prueba");
// Obtener todas las subclaves contenidas en esta:
String[] subKeys = rk1.GetSubKeyNames();
// Borrar una clave vacia:
rk.DeleteSubKey("Prueba");
// Borrar una clave recursivamente:
rk.DeleteSubKeyTree("Prueba");
Trabajando con key-values:
// Crea un key-value indicando su nombre, valor y tipo:
rk2.SetValue("ValorPrueba", 0,RegistryValueKind.String);
// Obtener todos los nombres de key-values que hay en una clave:
String[] values = rk2.GetValueNames();
// Obtener el valor de un key-value:
Console.WriteLine(rk2.GetValueKind("ValorPrueba").ToString());
// Obtener el tipo de dato de registro
// que contiene un key-value:
RegistryValueKind rvk = rk2.GetValueKind("ValorPrueba");
// Borrar un key-value:
// El parámetro boleano indica que salte una excepción
// si el key-value a borrar no existe.
rk2.DeleteValue("ValorPrueba", true);
Aunque los Setup Project que creamos con Visual Studio tiene capacidad para añadir claves y key-values al registro, su funcionalidad esta muy limitada, por ejemplo... no podriamos crear un key-value que contuviese el path de la aplicación que estemos instalando a no ser que lo pusieramos harcodeado.
El próximo 1 de Abril podréis asistir a un nuevo Webcast en el que podréis sacar el máximo provecho de sistemas, tanto nuevos como ya existentes, a través de Windows Communication Foundation. Para esta nueva charla, contaremos con la presencia de Hadi Hariri como ponente de excepción, que nos deleitará con todo lo que esta nueva herramienta nos puede ofrecer.
El evento será a las 19:30 - 21:30 (GMT+1), y como en las anteriores ocasiones, se retransmitirá vía Web a través de Live Meeting.
- Si no tienes Live Meeting, puedes descargarlo en el siguiente enlace.
- Podéis registraros en el evento en el siguiente enlace.
Yo para variar, me lo perderé en directo porque será mi segundo día en Dublin y tengo clase hasta las 19:00, pero lo bueno de estos eventos es poderlos ver a posteriori en diferido :D y este en concreto me interesa muchísimo.
Y no se confundan, Microsoft nada pinta aquí... al menos para mi, pero si para el SAT :D Sigo con mi lucha con la chatarra (parte I, parte II), alcanzado ahora nuevas cotas de asombro con el SAT. Una de mis reglas cuando hablo con el SAT, es ser tan humilde como pueda, prestar atención a lo que dice y evitar dar a entender que soy un usuario avanzado, en este caso... mucho menos decir que soy programador.
Una vez instalada la aplicación en red, la primera cosa que se me ocurre (yo y mis ocurrencias...), es lanzar el cliente como usuario Invitado ... a lo que recibo un error de que no puede escribir a un archivo que el programa de instalación ha dejado caer en C:\ ... nada más y nada menos, no se me ocurre otra cosa que llamar al SAT (más ocurrencias bobas...) para preguntar como ejecutar la aplicación con una cuenta desprivilegiada para que nadie pueda cargarse la máquina... la respuesta... increible:
"... es así y la aplicación necesita que se ejecute como administrador, es un requerimiento de Microsoft... es lo que hay..." [ ... ] "... vamos... deberia ser un usuario malintencionado..."
Aún estoy recuperandome del sock. Bien, pues siguiendo los consejos de Josue en Geeks.ms y mi colega Joe_Dalton de BandaAncha.st me lanzo a crear una Windows shell para contener al usuario en una interfaz de la que no puedan ejecutar nada, además deshabilito el Task Manager, ahora parece que todo irá bien y no hay posibilidad de catástrofe ya que los ordenadores no tienen teclado tampoco.
Comienzan los problemas de verdad, sin saber porque ... se cuelga "algo" en el servidor. Los clientes siguen funcionando, es decir... siguen accediendo a la bd en forma de archivos que hay en el recurso compartido y trabajando normalmente, pero en el servidor es imposible abrir el módulo de gestión ni abrir la aplicación cliente, obtengo errores de timeout y procesos colgados indefinidadmente, tampoco funciona el BDE Administrator del panel de control, nos vemos obligados a reiniciar la máquina... y vuelve a funcionar. Llamo al SAT (iluso yo) ... simplemente para preguntar como solventar el problema sin reiniciar la máquina, ya que eso supone que los clientes pierdan el trabajo no guardado ó hacerles que terminen, paren, reiniciar y volver a abrir de nuevo... con la mala imagen que da eso, es decir... que me diga algún servicio de Borland que reniciar ... no sé... algo xD, la respuesta tan asombrosa ó más como la anterior:
"... Microsoft no proveé de ninguna herramienta para hacer eso, es lo mismo que si se te cuelga alguna aplicación y tienes que reiniciar, no se puede hacer nada, Microsoft es así..."
Intentando recuperar la respiración, me reincorporo, me sacudo el polvo de la ropa después de haber rodado varias veces por el suelo y vuelvo a sentarme en la silla de nuevo, minutos después me llaman de la sala diciendo que hay un equipo que "no va". Me acerco y un mensaje alerta algo como "Cannot set focus on a unexistent window", doy a aceptar y la pantalla se queda sin datos... y sin que funcione ningún botón, toca matarla y reiniciarla. Llamo al SAT de nuevo ... "hola, soy el de antes", la respuesta... ya para llorar:
"... eso es porque el usuario va muy rápido, cuando se inicia el test sale una ventana de bienvenida y hay que darle a aceptar, si se pulsa sobre la ventana de fondo sin darle a aceptar... pasa eso..., la ventana esa se puso para ese fin, que terminase de cargar la otra... si le das antes ... pues normal que se quede asi..."
En fin... Ahora mismo, mientras escribo esto estaba esperando a que el SAT me coja de nuevo el teléfono porque hay otro cliente que se ha quedado ... literalmente congelado, ni el cronómetro que hay en una de las esquinas corre... pero justo ahora mismo, después de 5min congelado ha saltado el error : "Lock time out, table xxx.db" ...
¿Alguien tiene idea de que se puede hacer? Han transcurrido los 15 días de prueba ya... con lo que no creo que se pueda devolver... pero una cosa es que no te guste, otra cosa es ... ESTO. Este post se va a hacer interminable xD, ahora mismo estoy sacando un captura de pantalla de un volcado de datos (ó eso parece) sobre el formulario después de otro error de "Lock time out". No sé si reir ó llorar...
Aquí queda el testimonio de mi indignación e impotencia :(
En .NET 2.0 y WinForms había una forma que ahora ya no es del todo válida en WPF ya que han desaparecido el método Application.SetUnhandledExceptionMode y el evento Application.ThreadException, en su lugar tenemos Application.DispatcherUnhandledException:
<Application x:Class="KiosimWPF.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Window1.xaml"
DispatcherUnhandledException="App_DispatcherUnhandledException">
<Application.Resources>
</Application.Resources>
</Application>
Luego en el handler del evento podemos evitar que la excepción tumbe la aplicación:
void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{ // Proceso la excepción:
Trace.Write(String.Format("({0}):{1}\n\n{2}",e.Exception.GetType().Name, e.Exception.Message,
e.Exception.StackTrace));
// Evito que la excepción siga subiendo y tumbe
// la aplicación marcándola como manejada:
e.Handled = true;
}
Más info: Información general sobre la administración de aplicaciones (WPF).
Gracias a la lista de SNUG y a Toni Recio, me entero que hasta el 30 de Junio del 2008 Microsoft Learning ofrece un curso gratis, a parte de los que ya ofrecia "for FREE". Al registrarnos, se nos da un código con el que el precio del curso que elijamos se rebajará hasta $0 :) El código tiene validez por 30 días.
Remarcar que los siguientes cursos para desarrolladores son gratis ya de por sí, podriamos registrarnos en uno de pago mediante el descuento y realizar cualquiera de estos también:
Happy learning :)
Para determinadas situaciones, es más útil parametrizar nuestras aplicaciones desde los argumentos que pasamos al ejecutable que desde un archivo de configuración, sobre todo si es una aplicación de consola. El problema suele ser que lo que nos llega a nuestra aplicación es un array de cadenas y nosotros debemos relacionarlas entre ellas, pudiendo haber distintos tipos de elemetos como modificadores, parámetros, parámetros de los modificadores y distintas cantidades de ambos tipos de parámetros.
Este es un ejemplo de como ordenarlos obteniendo una estructura en la que todo esta ordenado en forma jerárquica para poder consultar fácilmente parámetros, modificadores y parámetros de los modificadores, la función devuelve una clase de este tipo:
public class CommandLineArguments
{ public List<String> Parameters { get; private set; } public Dictionary<String, List<String>> Modifiers { get; private set; }
public CommandLineArguments()
{ this.Parameters = new List<String>();
this.Modifiers = new Dictionary<String, List<String>>();
}
}
La función recorre el array de argumentos (args[]) identificando cada uno por su relación y posición y colocandolo en la clase anterior:
delegate void ModParFinder_(String key, Int32 current);
static CommandLineArguments ParseParameters(String[] args, Char modIndicator)
{ CommandLineArguments cmdArgs = new CommandLineArguments();
// Busca los parametros de un modificador y los
// añade a la clave dada
ModParFinder_ ModParFinder =
new ModParFinder_(delegate(String key, Int32 current)
{ current++;
if (!cmdArgs.Modifiers.ContainsKey(key))
cmdArgs.Modifiers.Add(key, new List<String>());
while (current < args.Length && !args[current].StartsWith(modIndicator.ToString()))
{ cmdArgs.Modifiers[key].Add(args[current]);
current++;
}
});
Boolean modsFound = false;
if (args != null)
for (Int32 i = 0; i < args.Length; i++)
{ if (args
.StartsWith(modIndicator.ToString()))
{ ModParFinder.Invoke(args
, i);
modsFound = true;
}
else if (!modsFound)
cmdArgs.Parameters.Add(args
);
}
return cmdArgs;
}
Para usar y consultar esta funcionalidad, un sencillo ejemplo:
static Int32 Main(String[] args)
{ Console.WriteLine(Environment.CommandLine);
CommandLineArguments cmdL = ParseParameters(args, '-');
// Mostrar la información recabada
Console.WriteLine("Parameters: "); foreach (String s in cmdL.Parameters)
{ Console.WriteLine(" " + s); }
Console.WriteLine("Modifiers: "); foreach (String k in cmdL.Modifiers.Keys)
{ Console.WriteLine(k+": ");
foreach (String s in cmdL.Modifiers[k])
Console.WriteLine(" " + s); }
Console.ReadKey(true);
return 0;
}
Por ejemplo, si ejecutamos la aplicación y le pasamos los siguientes argumentos:
MiApp.exe "C:\Archivos de programa\path largo" -s smod -l "lmod largo" 123 -w wmod1 wmod2 -h -j
El resultado será el siguiente:
Parameters:
C:\Archivos de programa\path largo
Modifiers:
-s:
smod
-l:
lmod largo
123
-w:
wmod1
wmod2
-h:
-j:
Luego el validar la cantidad y corrección de los parámetros y modificadores... a gusto de cada uno :D
Para dar el foco a una aplicación externa a la nuestra, se usa el método SetForegroundWindow de user32.dll mediante PInvoke:
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
Simplemente hay que encontrar el proceso y pasar a este método el puntero Process.MainWindowHandle, que apunta a la ventana principal de la aplicación, por ejemplo asi:
Process runningProc = Process.GetProcessesByName("notepad").First(); SetForegroundWindow(runningProc.MainWindowHandle);
Ojo que no es el mismo que Process.Handle:)
Existe un pequeño... inconveniente... cuando el proceso que queremos controlar acaba de iniciarse. Es posible que algunas veces no funcione, ya que el MainWindowHandle podría no estar configurado ó la aplicación no ha terminado de arrancar... y no se ha enganchado todavia al bucle de mensajes de la GUI (por lo que enviarle el mensaje de foco seria inútil). Para solventar este problema, podemos hacer uso del método Process.WaitForInputIdle que espera a que la aplicación este en estado Idle y por tanto... esperando mensajes de la cola:
Process newProc = Process.Start("notepad"); newProc.WaitForInputIdle(1000);
SetForegroundWindow(newProc.MainWindowHandle);
Espero que sea de utilidad.
Estoy empezando a pensar que esto es una broma de camara oculta ó similar. Sigo en mi lucha con el maravilloso programa para hacer test. Realmente no sé que pensar ya, supongo que los test que incluye son buenos ... porque lo que es el programa... tela...
Ya están hechos los agujeros en la pared para pasar los cables... y llegan las pruebas de funcionamiento en red, desinstalo la aplicación ya que no hay forma de pasar de monopuesto a multipuesto y vuelvo a reinstalar:
- La desinstalación del programa no funciona completamente, si lo instalas en un modo (local) y luego lo quieres reinstalar en otro (en red)... has de ir borrando archivos del hdd porque no desinstala bien y quedan archivos residuales de configuración en "Archivos de Programa".
- Cuando eliges una instalación en red, te instala la base de datos en local... aunque ese PC luego se vaya a conectar a un servidor.
- La carpeta del servidor donde esta la BD... tiene que estar compartida y con todos los permisos para los usuarios de las máquinas clientes ... es decir... CUALQUIER USUARIO PODRÍA BORRAR LA BD!! El servicio técnico me ha contestado a esto... "tendría que ser un usuario malintencionado"... en fin, del riego va a ser.
En fin, y las máquinas clientes con XP Home Edition :( Al final ó encuentro la forma buena de reponer gpedit.msc en este sistema operativo ó me tocará pagar el upgrade a XP Professional Edition, ó instalar un programa para cibercafes ó yo que sé, pero si no como a alguien se le ocurra acceder al servidor por red... que risa :|
El problema:
La red quedará dividida en dos, una donde están todos los equipos para realizar test y conectados a eth1 del servidor; otra donde esta el router ADSL al que el servidor esta conectado mediante eth0 . El servidor NO comparte la conexión. Todos los equipos tienen XP HE y por lo tanto es una red en grupo de trabajo con las limitaciones propias de estos sistemas operativos, necesito que los usuarios Invitados NO puedan acceder a la red mediante el explorador de Windows... aunque la carpeta este compartida y con permisos para todo dios.
Cualquier idea sera bienvenida y agradecida :)
Existe una extensión llamada FFClickOnce para poder usar ClickOnce desde Firefox. Una buen tip para los usuarios de este navegador... como yo :P Más info en la página del proyecto.
Vía el blog de Jorge Serrano.
Mi padre, que tiene una autoescuela, acaba de adquirir un software para que los alumnos realizen test de una de las principales (si no la más famosa) editoriales españolas especializadas que seguro todos conoceis, aunque no diré el nombre. El caso es que se ha decidido adquirir un nuevo software por cambiar de aires, ya que siempre lo mismo ... no da muy buena imagen, asi que esto se ha planteado como una inversión... no precisamente barata.
Actualmente, estaba trabajando con un programa que adquirió en 1995, corria sobre Windows 3.11, esta hecho en la versión de Visual Basic de la época y que cumplia correctamente su función. Además presenta una interfaz intuitiva que hacia que gente profana en la informática, como mis padres... pudieran trabajar con el fácilmente, es altamente configurable y la información, test, ayuda e imágenes estan bien estructuradas facilitando la creación de nuevos tests. La única posible pega... era que los usuarios tienen sus datos en un disquete con su nombre que deben utilizar para iniciar sesión ... y bueno, el feo aspecto grisáceo pseudo-3D que tiene una aplicación Windows :D
Este tipo de programas no evoluciona mucho, ya que funcionalmente siempre son lo mismo:
- Una pantalla de inicio de sesión.
- Una pantalla donde el alumno puede revisar su evolución con los test realizados, las preguntas falladas, ..etc...
- Una pantalla de selección de test.
- Una pantalla que muestra preguntas con 3 respuestas donde solo una es la correcta, puede haber una imagen y una ayuda opcional que explica la pregunta en si.
Adicionalmente hay un módulo específico para el profesor, protegido con contraseña, donde:
- Puede confeccionar test.
- Puede confeccionar nuevas preguntas, con sus 3 respuestas, la respuesta que es correcta y la imagen si procede.
- Imprimir test.
- Gestionar alumnos.
Realmente un programa asi es bastante sencillo de hacer con por ejemplo .NET y SQL Server 2005 Ex, el problema son los test... normalmente un recurso bajo copyright, que no puedes copiar.
Este nuevo programa, tiene la misma funcionalidad, ó al menos eso dice, más vistoso... y la posibilidad de centralizar los usuarios en una BD de Borland en lugar de que los alumnos tengan que ir con disquetes. Es comenzar a usar el nuevo programa y decir... NO ME LO PUEDO CREER!! , este es el resultado de unas dos horas de uso:
- Borras un usuario, lo vuelves a crear y todos los datos sobre tests realizados y demás ... persisten!
- Al crear un nuevo alumno, ningún dato es obligatorio, lo que provoca que si omites algún dato que debería serlo, al intentar iniciar sesión&n