Código fuente: https://github.com/batressc/DemoMVVM
¿Windows Forms en 2022? Pues… sí (y con .NET Framework). A pesar de que estamos en una generación donde el desarrollo en la nube, web y movil son «la punta de lanza», existen empresas que por diversos motivos (seguridad, rendimiento o especializaciones de la UI/UX) aún requieren trabajar sobre aplicaciones on-premise y con aplicaciones de escritorio.
Para facilitar el desarrollo de estas aplicaciones, DevExpress proporciona diferentes componentes, servicios y frameworks que agregan funcionalidades que nos permiten ahorrar horas de desarrollo y codificación, brindando una interfaz amigable donde debemos únicamente configurar propiedades o agregar pocas líneas de código para que funcionen sus componentes o servicios.
Recientemente tuve la oportunidad de implementar una aplicación utilizando WinForms (.NET Framework), para facilitar el desarrollo decidí agregar la inyección de dependencias que proporciona Microsoft y el Framework MVVM de DevExpress. Si bien al principio tuve algunos problemas para realizar la integración de las tecnologías, estoy satisfecho con el resultado final.
En este artículo se muestra mediante un pequeño formulario de ingreso de datos los pasos a realizar para integrar estas tecnologías, a la vez que detallo las condiciones y aspectos que determinaron la implementación final de la integración.
Para el desarrollo de la aplicación se utilizan las siguientes tecnologías y herramientas de desarrollo:
- .NET Framework 4.8
- Microsoft.Extensions.Hosting 6+
- DevExpress WinForms 21.2.4 (WinForms MVVM Framework está incluido)
- Visual Studio 2022 Professional
NOTA: Si bien este es un artículo de paso a paso, no es un tutorial introductorio. Asumo que el lector posee conocimientos básicos de WinForms, el manejo de inyección de dependencias en .NET 6/Core, el uso del Framework MVVM de DevExpress y sus asistentes, adicional a ello que se posee una versión de evaluación o pagada de los componentes de WinForms y utiliza Visual Studio como IDE de desarrollo.
Creación del proyecto WinForms + MVVM
El primer paso es crear nuestro proyecto de WinForms utilizando el asistente de DevExpress. Para identificar la opción de proyecto de forma más rápida podemos aplicar los filtros: «C#» y «DevExpress» en las casillas de «Lenguajes» y «Tipos de proyectos» respectivamente del selector de plantillas de Visual Studio o buscar por las palabras claves «WinForms App» en el cuadro de texto.
En el siguiente cuadro de diálogo, indicamos el nombre del proyecto y la carpeta contenedora de la solución de Visual Studio. En mi caso el nombre del proyecto es «DemoMVVM».
Al presionar el botón «Crear» aparece el cuadro de diálogo del selector de plantillas de DevExpress. Corroboramos que las características seleccionadas son las siguientes:
- Lenguaje C#
- Plantillas para .NET Framework y versión de .NET Framework la más actualizada posible
- Validamos que el nombre del proyecto sea el que escribimos anteriormente
- Seleccionamos «Blank Application» del grupo «WINFORMS COMMON»
- Marcamos la casilla «MVVM Ready», la cual agrega todas las referencias necesarias para poder utilizar el Framework MVVM.
Una vez todo está listo presionamos el botón «Create Project».
Una vez finalizado el proceso de generación de la solución, tenemos una aplicación de WinForms con los archivos MainViewModel.cs
y MainView.cs
, los cuales ya están asociados automáticamente mediante el control del tipo MVVMContext
para funcionar según el Framework MVVM de DevExpress.
Para validar que todo está correcto, recompilamos la aplicación y confirmamos que no se muestren mensajes de error en la consola de salida.
Agregando inyección de dependencias
Para agregar esta funcionalidad utilizamos un Host genérico de .NET adaptando la invocación del método Host.CreateDefaultBuilder
en Program.cs
. Este método realiza diversas configuraciones que son utilizadas en las aplicaciones de .NET 6/Core:
- Agrega los servicios de registros de logs
- Habilita la carga de configuraciones mediante
appsettings.json
y sus variantes por variables de entorno - Proporciona el método
ConfigureServices
donde se define la configuración de la inyección de dependencias.
Volviendo a nuestro proyecto, abrimos el gestor de paquetes NuGet, buscamos e instalamos el paquete Microsoft.Extensions.Hosting
.
Una vez se ha instalado el paquete, abrimos el archivo Program.cs
y realizamos las siguientes modificaciones:
NOTA: los métodos Application.EnableVisualStyles
y Application.SetCompatibleRenderingDefault
deben ejecutarse antes de crear el formulario principal, esto para evitar una excepción del tipo InvalidOperationException
.
Para finalizar agregamos un archivo appsettings.json
en la raíz del proyecto, del cual tomaremos un par de valores de configuración para demostrar que la inyección de dependencia está funcionando, también definimos los niveles de notificación de los servicios de logs de la aplicación.
Una vez creado el archivo appsettings.json
presionamos click derecho sobre él y seleccionamos la opción «Propiedades». Luego cambiamos la propiedad Copiar en el directorio de salida
de No copiar
a Copiar si es posterior
.
Para validar que todo está correcto hasta este punto, recompilamos y ejecutamos nuestra aplicación, la cual debe mostrar un formulario vacío.
Contenido de MainView.cs
Vamos a crear un formulario simple, el cual posee dos textos de entrada: El nombre de un sitio web y su Url, tiene además un área de texto y un botón, donde al presionarse devuelve un objeto JSON con los valores ingresados en el área de texto.
Eres libre de realizar el diseño del formulario ya sea con controles de WinForms o DevExpress. En mi caso, utilizando los controles del DevExpress obtengo un diseño de esta forma:
Elemento | Tipo | Propiedades |
Website Name | textEdit |
|
Website Url | textEdit |
|
JSON | memoEdit |
|
Show JSON | simpleButton |
|
WinForms MVVM
Ahora que tenemos listo nuestro formulario vamos a indicar los enlaces y comandos necesarios entre la View y su correspondiente ViewModel.
Lo primero que haremos es agregar 3 propiedades en MainViewModel.cs
de tipo string
para realizar el enlace con los controles de nombre/url del sitio web y datos JSON del formulario, también definiremos un comando que permitirá ejecutar la acción de transformar nuestros datos de entrada en un objeto JSON.
Luego especificaremos en MainView.cs
la configuración de los enlaces y comandos. El código del formulario debe ser similar al siguiente:
Con estos cambios, ya podemos recompilar y ejecutar nuestra aplicación para ver su funcionamiento. Si escribimos en los textos Luis Fernández
y https://batressc.com
en Website Name
y Website Url
respectivamente y presionamos el botón «Show JSON» obtenemos el siguiente resultado:
Acerca de las Views y ViewModels
Antes de utilizar la inyección dependencias en nuestras ViewModels, primero debemos tener claros los siguientes aspectos del funcionamiento del Framework MVVM de DevExpress:
- La asociación de la View y su ViewModel se realiza mediante el control
MVVMContext
. - De forma predeterminada, el control
MVVMContext
necesita un constructor sin parámetros para poder crear las instancias de las ViewModels. - El Framework MVVM no utiliza instancias de las ViewModels definidas en tiempo de diseño, sino que utiliza tipos de datos especiales generados en tiempo de ejecución los cuales son una copia de las ViewModels pero poseen características que permiten aprovechar todas las funcionalidades del framework.
Haciendo un especial énfasis al último aspecto, si ponemos un punto de interrupción luego de la asignación de la variable fluent
en MainView.cs
podemos observar que el tipo de datos de la ViewModel asociada posee la siguiente nomenclatura:
DemoMVVM.MainViewModel_{guid}
Estos tipos de datos autogenerados se almacenan en un ensamblado dinámico que se crea en tiempo de ejecución. Si revisamos mediante reflexión el ensamblado que contiene al tipo de datos, obtendremos un nombre con la siguiente nomenclatura:
DevExpress.Mvvm.{version}.DynamicTypes.{guid}
Por el momento, tengamos presente estas observaciones, ya que influyen en la implementación de la inyección de dependencias de nuestro proyecto.
Problemas al usar inyección de dependencias
Realizaremos un cambio en la aplicación para que al iniciar lea los valores del archivo appsettings.json
y los muestre en los controles de texto de Website Name
y Website Url
. Para ello, tenemos que establecer valores inciales a las propiedades WebsiteName
y WebsiteUrl
de la clase MainViewModel
, esto con ayuda de la inyección de IConfiguration
y el método GetValue<T>
, el cual nos permitirá extraer los valores correspondientes.
También en Program.cs
debemos agregar la dependencia de MainViewModel
en el método ConfigureServices
que hemos definido para ello.
Una vez aplicadas las modificaciones nuestros archivos MainViewModel.cs
y Program.cs
quedan de la siguiente forma:
Si recompilamos nuestra aplicación el proceso se ejecuta correctamente, lo que indica que no tenemos errores de codificación, pero al momento de ejecutarla se nos muestra el siguiente mensaje de error:
Unable to create a ViewModel of the specified type: MainViewModel. There are no constructors that match these input parameters: No Parameters
Como vimos en la sección anterior, el Framework MVVM necesita un constructor sin parámetros para crear la instancia del tipo dinámico de la ViewModel correspondiente, ahora que hemos agregado la dependencia IConfiguration
en MainViewModel
rompemos este esquema. ¿Qué podemos hacer para solventarlo?
Revisando las alternativas…
En la documentación oficial de DevExpress se muestran tres alternativas que podemos implementar para generar nuestras ViewModels en tiempo de ejecución. De estas alternativas solamente revisaremos el uso del método ViewModelSource.Create<T>
y los eventos ViewModelCreate
, el uso de la clase ViewModelBase
está fuera del alcance de este artículo ya que se pierden algunas funcionalidades y no se recomienda su uso.
Método ViewModelSource.Create<T>
La clase ViewModelSource
posee diversos métodos auxiliares que nos permiten crear instancias de nuestras ViewModels ya sea especificando un tipo de datos o invocando el constructor (con o sin parámetros) del ViewModel que necesitamos.
Para el caso del método Create<T>
, examinando sus implementaciones, vemos que se definen dos sobrecargas:
- La primera realiza una invocación del método genérico
Factory<T>
el cual utiliza el constructor predeterminado del tipo de datosT
, dondeT
corresponde al tipo de datos de la ViewModel. - El segundo método posee una serie de validaciones preliminares, pero podemos resumir su funcionamiento en que recibe como parámetro una expresión la cual debe utilizar uno de los constructores con o sin parámetros definidos en la ViewModel.
Con estos insumos y restricciones, una primera implementación que podemos plantear es realizar la inyección de la dependencia IConfiguration
en MainView
y utilizarla como parámetro al invocar el constructor de MainViewModel
con el método ViewModelSource.Create<T>
. Aplicamos las modificaciones al archivo MainView.cs y obtenemos lo siguiente:
Si recompilamos y ejecutamos nuestra aplicación, veremos que al iniciar los cuadros de texto tendrán los textos Luis Fernández
y https://batressc.com
, lo cual corrige el problema de nuestra ViewModel pero de una forma accidentada: Si bien confirmamos que utilizamos la inyección de dependencias al recuperar los valores de appsettings.json
mediante IConfiguration
las dependencias en MainViewModel
las tuvimos que resolver de forma manual. Asumiendo que es aceptable para este caso simple, si definimos otras ViewModels que requieran en un esquema de dependencias de dos o tres niveles, con 4 o 5 servicios que a su vez requieran inyección de dependencias la situación se vuelve inmanejable.
Eventos ViewModelCreate
Internamente el Framework MVVM cuando no logra obtener una instancia del ViewModel mediante el constructor predeterminado ejecuta los eventos ViewModelCreate
, estos eventos poseen un parámetro de tipo ViewModelCreateEventArgs
el cual posee las propiedades ViewModelType
y RuntimeViewmodelType
que permiten determinar en tiempo de ejecución el tipo de dato original y dinámico respectivamente de la ViewModel que requerimos, también expone la propiedad ViewModel
para que podamos asignar la instancia de la ViewModel que generemos.
Existen dos versiones de este evento:
- Local, por medio del evento
MVVMContext.ViewModelCreate
, esta puede accederse mediante el control MVVMContext que se asigne en cada View. - Global, a través del evento estático
MVVMContextCompositionRoot.ViewModelCreate
.
Debemos de tener en cuenta que ambos eventos se lanzan durante la ejecución de la aplicación, primeramente la versión estática y luego la versión local, lo que nos permite poder establecer en la versión estática el comportamiento general para la creación de ViewModels y si tenemos un caso particular que no se adapta a ese esquema, poder sobreescribirlo con la implementación local.
Con estas observaciones, al revertir los cambios en MainView.cs
y realizar los siguientes en Program.cs
obtenemos el siguiente resultado:
De los cambios aplicados podemos mencionar:
- Como vimos en una sección anterior, el Framework MVVM utiliza una copia generada dinámicamente en tiempo de ejecución de los tipos de datos de las ViewModels. Para obtener el tipo de dato equivalente dinámico podemos ayudarnos del método
ViewModelSource.GetPOCOType
, el cual solamente requiere como parámetro el tipo de dato de la ViewModel. - Ahora que tenemos un mecanismo para obtener el tipo dinámico, podemos realizar la configuración de nuestras dependencias especificando mediante los métodos de extensión
AddTransient
,AddScoped
oAddSingleton
que requerimos que se genere una instancia de nuestro tipo dinámico al hacer la inyección de la dependencia de su ViewModel relacionada. - Finalmente registramos un manejador de eventos para la versión estática de
ViewModelCreate
el cual asignará nuestras instancias de las ViewModels utilizando el métodoGetRequiredService
.
Como vemos esta es una solución más elegante, ya que evitamos resolver las dependencias de forma manual, además evitamos realizar inyecciones de dependencias en las Views así como la asignación manual de la ViewModel en el control MVVMContext
.
Excelente post.
Muy detallado y basado en buenas prácticas en el desarrollo
Muchas gracias estimado. Saludos