Mayo de 2019

Volumen 34, número 5

[El programador ocupado]

Programación con Naked: Colecciones Naked

Por Ted Neward | Mayo de 2019

Ted NewardBienvenidos de nuevo, usuarios de NOF. La última vez, aumenté el tipo de dominio Speaker con varias propiedades, junto con cierta cantidad de anotaciones y convenciones sobre las propiedades que proporcionan sugerencias (o, para ser más sinceros, instrucciones) a la interfaz de usuario sobre cómo validar o presentar esas propiedades al usuario. Sin embargo, una cosa que no incluí es cómo un objeto de dominio concreto puede tener referencias a más de una instancia de algo. Por ejemplo, los ponentes (speakers) pueden tener varias charlas (talks) que pueden presentar o tener experiencia en uno o más temas (topics). NOF se refiere a estos elementos como "colecciones" y existen varias reglas respecto a su funcionamiento que son ligeramente distintas de la conversación anterior.

A continuación veremos cómo asignar a los ponentes algunas charlas y temas.

Conceptos de Naked

Para adentrarse en este territorio, deje atrás todas las matrices. NOF no usa matrices para las propiedades de la colección, sino que se basa completamente en objetos de colección (IEnumerable<T>-derived) para contener valores de cero a muchos de algún otro tipo. Según el manual de NOF, es muy recomendable que estas colecciones estén fuertemente tipadas (mediante genéricos) y NOF no permite varias asociaciones de tipos de valor (como cadenas, tipos enumerados, etc.) porque cree que, si el tipo es bastante "importante" para garantizar la asociación, debe ser un tipo de dominio completo.

Por lo tanto, si quiere capturar la noción de un tema (como "C#", "Java" o "sistemas distribuidos") en el sistema de conferencias, donde otros enfoques de programación pueden permitirle conseguirlo con un simple valor de "list-of-strings" como tipo de propiedad, NOF insiste en que el tema sea un tipo de objeto de dominio completo (es decir, una clase pública con propiedades) junto con sus propias reglas de dominio. Sin embargo, es razonable que la lista de temas sea un conjunto fijo, así que inicializaré la base de datos con el conjunto de temas completo que quiera considerar en mi conferencia.

Del mismo modo, aunque "charla" podría ser solo un título, en realidad es una serie de cosas: un título, una descripción, un tema (al que pertenece o hace referencia) y la presentan uno o varios ponentes. Claramente, aún tengo cierto modelado de dominio por delante.

Colecciones Naked

En muchos sentidos, la manera más fácil de empezar son las clases de dominio para las charlas y los temas, sin especificar las conexiones entre ellos (o ponentes). De momento, gran parte de lo que escribo aquí para cada uno de ellos debería ser bastante trivial y directo, como se ve en la figura 1.

Figura 1 Clases de dominio para charlas y temas

public class Talk
  {
    [NakedObjectsIgnore]
    public virtual int Id { get; set; }
    [Title]
    [StringLength(100, MinimumLength = 1,
       ErrorMessage = "Talks must have an abstract")]
    public virtual string Title { get; set; }
    [StringLength(400, MinimumLength = 1,
       ErrorMessage = "Talks must have an abstract")]
    public virtual string Abstract { get; set; }
  }
  public class TalkRepository
  {
    public IDomainObjectContainer Container { set; protected get; }
    public IQueryable<Talk> AllTopics()
    {
      return Container.Instances<Talk>();
    }
  }
  [Bounded]
  public class Topic
  {
    [NakedObjectsIgnore]
    public virtual int Id { get; set; }
    [Title]
    [StringLength(100, MinimumLength = 1,
       ErrorMessage = "Topics must have a name")]
    public virtual string Name { get; set; }
    [StringLength(400, MinimumLength = 1,
       ErrorMessage = "Topics must have a description")]
    public virtual string Description { get; set; }
  }
  public class TopicRepository
  {
    public IDomainObjectContainer Container { set; protected get; }
    public IQueryable<Topic> AllTopics()
    {
      return Container.Instances<Topic>();
    }
  }

De momento, este proceso es bastante directo. Obviamente, hay otras cosas que podría o debería agregar a cada una de estas dos clases, pero esto cumple su función bastante bien. El nuevo atributo usado, [Bounded], indica a NOF que la lista completa (e inmutable) de instancias puede y debe conservarse en memoria en el cliente y presentarse al usuario como una lista desplegable de la que elegir opciones. En consecuencia, a continuación, es necesario establecer la lista completa de temas en la base de datos, lo que se hace más fácilmente en la clase DbInitializer del proyecto “SeedData”, en el método Seed (descrito en columnas anteriores de esta serie), como se muestra en la figura 2 .

Figura 2 Creación de una lista de temas

protected override void Seed(ConferenceDbContext context)
{
  this.Context = context;
  Context.Topics.Add(new Topic() { Name = "C#",
    Description = "A classical O-O language on the CLR" });
  Context.Topics.Add(new Topic() { Name = "VB",
    Description = "A classical O-O language on the CLR" });
  Context.Topics.Add(new Topic() { Name = "F#",
    Description = "An O-O/functional hybrid language on the CLR" });
  Context.Topics.Add(new Topic() { Name = "ECMAScript",
    Description = "A dynamic language for browsers and servers" });
  Context.SaveChanges();
  // ...
}

Esto proporciona una lista de temas (bastante pequeña, pero útil) desde la que trabajar. Por cierto, si juega con eso en casa y escribe el código a mano, recuerde agregar TalkRepository al menú principal incorporándolo al método MainMenus en NakedObjectsRunSettings.cs del proyecto de servidor. Además, asegúrese de que se muestren los dos tipos de repositorio en el método Services del mismo archivo.

Básicamente, un ponente presenta una charla sobre un tema concreto. Con motivos de simplicidad, de momento, voy a omitir el escenario más complicado en que presentan una charla dos ponentes o una charla tiene varios temas. Por lo tanto, para el primer paso, vamos a agregar las charlas para el orador:

private ICollection<Talk> _talks = new List<Talk>();
public virtual ICollection<Talk> Talks
{
  get { return _talks; }
  set { _talks = value; }
}

Si compila y ejecuta el proyecto, verá que "Talks" se muestra como una colección (tabla) en la interfaz de usuario, pero estará vacía. Por supuesto, podría agregar algunas charlas en SeedData, pero, en general, los ponentes deben poder agregar charlas a sus perfiles.

Acciones de Naked

Para hacerlo, se puede agregar una acción a la clase Speaker: un método que aparecerá "por arte de magia" como un elemento seleccionable en el menú "Actions" (Acciones) cuando se muestre un objeto Speaker en la pantalla. Como las propiedades, las acciones se detectan a través de la magia de la reflexión, de modo que solo necesita crear un método público en la clase Speaker:

public class Speaker
{
  // ...
  public void SayHello()
  {
  }
}

Ahora, cuando se compila y ejecuta, después de traer a colación la clase Speaker, se muestra el menú "Actions" y, dentro de este, aparece "SayHello". Actualmente no hace nada. Sería bueno, como punto de partida, colocar al menos un mensaje para el usuario. En el mundo de NOF, para hacer esto, se usa un servicio, un objeto cuya finalidad es proporcionar cierta funcionalidad adicional que no pertenezca a ningún objeto de dominio concreto. En el caso de los "mensajes al usuario" generales, se usa un servicio genérico que define el propio NOF en la interfaz IDomainObjectContainer. Sin embargo, necesito una instancia de uno de estos para poder hacer algo y NOF usa la inserción de dependencias para proporcionarla a petición: Declare una propiedad en la clase Speaker del tipo IDomainObjectContainer y NOF se asegurará de que cada instancia tenga una:

public class Speaker
{
  public TalkRepository TalkRepository { set; protected get; }
  public IDomainObjectContainer Container { set; protected get; }

El objeto Container tiene un método "InformUser" que se usa para transmitir mensajes generales al usuario, por lo que su uso desde la acción SayHello es tan sencillo como:

public class Speaker
{
  // ...
  public void SayHello()
  {
    Container.InformUser("Hello!");
  }
}

Pero empecé con el deseo de permitir al usuario agregar una charla al repertorio de un ponente determinado. En concreto, necesito capturar el título de la charla, el resumen o descripción (recuerde que "abstract" es una palabra reservada en C#), y el tema al que pertenece la charla. Llamaré a este método "EnterNewTalk". A continuación, tengo la siguiente implementación:

public void EnterNewTalk(string title, string description, Topic topic)
{
  var talk = Container.NewTransientInstance<Talk>();
  talk.Title = title;
  talk.Abstract = description;
  talk.Speaker = this;
  Container.Persist<Talk>(ref talk);
  _talks.Add(talk);
}

Aquí suceden varias cosas que vamos a considerar. En primer lugar, uso IDomainObjectContainer para crear una instancia transitoria (no persistente) de la charla. Esto es necesario porque NOF debe poder insertar "enlaces" en cada objeto de dominio para poder hacer su magia. Por eso todas las propiedades deben ser virtuales: para que NOF pueda administrar la sincronización de la interfaz de usuario al objeto, por ejemplo. A continuación, se establecen las propiedades de la charla y se vuelve a usar el contenedor para la persistencia de la charla. Si no se hace, la charla no es un objeto persistente y no se almacenará cuando agregue la charla a la lista del ponente.

Sin embargo, es justo preguntarse cómo especificó esta información el usuario en el propio método EnterNewTalk. De nuevo entran en acción las maravillas de la reflexión: NOF extrae los nombres y tipos de parámetros de los parámetros de método y construye un cuadro de diálogo para capturar dichos elementos, incluido el propio tema. ¿Recuerda cuando se anotó Topic con "Bounded"? Eso indicó a NOF que debía crear la lista de temas en este cuadro de diálogo para que fuera una lista desplegable y resultara increíblemente fácil seleccionar un tema de la lista. En este punto, debería ser fácil inferir que, a medida que agregue temas al sistema, se agregarán a esta lista desplegable sin necesidad de hacer nada más.

Ahora es razonable sugerir que el repositorio TalksRepository debería admitir la creación de charlas, en lugar de hacerlo en la propia clase Speaker y, como se aprecia en la figura 3, es una refactorización fácil de hacer.

Figura 3 Creación de charlas compatible con TalkRepository

public class Speaker
  {
    public TalkRepository TalkRepository { set; protected get; }
    public IDomainObjectContainer Container { set; protected get; }
    public void EnterNewTalk(string title, string description, Topic topic)
    {
      var talk = TalkRepository.CreateTalk(this, title, description, topic);
      _talks.Add(talk);
    }
  }
  public class TalkRepository
  {
    public IDomainObjectContainer Container { set; protected get; }
    public Talk CreateTalk(Speaker speaker, string title, string description,
                Topic topic)
    {
      var talk = Container.NewTransientInstance<Talk>();
      talk.Title = title;
      talk.Abstract = description;
      talk.Speaker = speaker;
      Container.Persist<Talk>(ref talk);
      return talk;
    }
  }

Además, al hacerlo, el menú "Talks" se adorna automáticamente con un nuevo elemento de menú, "CreateTalk", que, de nuevo, gracias a la magia de la reflexión, creará un cuadro de diálogo automáticamente para introducir los datos necesarios para crear una charla. Por supuesto, los ponentes no pueden escribirse sin más, por lo que NOF hará que sean un campo desplegable, lo que significa que NOF espera que se arrastre y coloque un objeto Speaker en este campo. Para ver esto en acción en la interfaz Gemini de NOF, inicie la aplicación, seleccione un ponente y, a continuación, haga clic en el botón para cambiar de panel (el botón de doble flecha de la parte inferior de la pantalla). El ponente seleccionado cambiará a la derecha de la pantalla y se mostrará la interfaz principal, lo que permitirá la selección del elemento "Talks/Create Talk". Arrastre el nombre del ponente al campo Speaker del cuadro de diálogo Create Talk (Crear charla) y voilà: se seleccionó el ponente.

Es absolutamente fundamental entender lo que ocurre aquí: ahora tengo dos rutas de acceso de interfaz de usuario distintas (creación de una charla en el menú de nivel superior o desde un ponente determinado) que permiten dos escenarios de navegación de usuario diferentes con muy poco esfuerzo y sin duplicación. TalkRepository se ocupa de todo lo relacionado con "CRUD" y "Talk", el ponente usa dicho código y la interacción con el usuario se mantiene completamente desde el ponente si es la disposición que quiere el usuario.

Resumen

Esto no es el kit de herramientas de la interfaz de usuario de su abuelo. En unas pocas líneas de código, he conseguido una interfaz en la que se puede trabajar (y, teniendo en cuenta que los datos se almacenan y recuperan desde un back-end SQL estándar, un sistema de almacenamiento de datos) que podrían utilizar los usuarios directamente, al menos por el momento. Y, lo más importante, nada de esto tiene un formato ni un lenguaje propietario: esto es C# directo y SQL Server directo, y la propia interfaz de usuario es Angular. Hay algunas cosas más que abordar acerca de la interfaz predeterminada de NOF, de las que me ocuparé en el próximo artículo, como las opciones de autenticación y autorización. Pero, mientras tanto… ¡Que disfrute programando!


Ted Neward es un consultor de politecnología, orador y mentor residente en Seattle. Ha escrito una gran cantidad de artículos, es autor y coautor de una docena de libros y realiza conferencias por todo el mundo. Puede ponerse en contacto con él en ted@tedneward.com o leer su blog en blogs.tedneward.com.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Richard Pawson


Comente este artículo en el foro de MSDN Magazine