Desarrollo multiplataforma con .NET, programación reactiva, patrón MVVM y generación de código

MVVM reactivo y estándar .NET

Hoy en día, la plataforma .NET es una herramienta verdaderamente universal: con su ayuda puede resolver una amplia gama de tareas, incluido el desarrollo de aplicaciones para sistemas operativos populares, como Windows, Linux, MacOS, Android e iOS. En este artículo, veremos la arquitectura de las aplicaciones .NET multiplataforma utilizando el patrón de diseño MVVM y la programación reactiva . Nos familiarizaremos con las bibliotecas ReactiveUI y Fody , aprenderemos cómo implementar la interfaz INotifyPropertyChanged usando atributos, tocaremos los conceptos básicos de AvaloniaUI , Xamarin Forms , Universal Windows Platform , Windows Presentation Foundation y .NET Standard , y aprenderemos herramientas efectivas para probar modelos de capas y modelos de presentación de aplicaciones.

El material es una adaptación de los artículos " MVVM reactivo para la plataforma .NET " y " Aplicaciones .NET multiplataforma a través del enfoque MVVM reactivo ", publicado por el autor anteriormente en el recurso Medio. El código de muestra está disponible en GitHub .

Introduccion Arquitectura MVVM y .NET multiplataforma


Al desarrollar aplicaciones multiplataforma en la plataforma .NET, debe escribir código portátil y compatible. Si trabaja con marcos que usan dialectos XAML, como UWP, WPF, Xamarin Forms y AvaloniaUI, esto se puede lograr utilizando el patrón de diseño MVVM, la programación reactiva y la estrategia de separación de código estándar .NET. Este enfoque mejora la portabilidad de las aplicaciones al permitir a los desarrolladores utilizar una base de código común y bibliotecas de software comunes en varios sistemas operativos.

Echaremos un vistazo más de cerca a cada una de las capas de una aplicación construida sobre la base de la arquitectura MVVM: el modelo, la vista y el modelo de vista. La capa del modelo representa servicios de dominio, objetos de transferencia de datos, entidades de bases de datos, repositorios: toda la lógica empresarial de nuestro programa. La vista es responsable de mostrar los elementos de la interfaz de usuario en la pantalla y depende del sistema operativo específico, y el modelo de presentación permite que las dos capas descritas anteriormente interactúen, adaptando la capa del modelo para interactuar con el usuario humano.

La arquitectura MVVM proporciona la división de responsabilidades entre las tres capas de software de la aplicación, por lo que estas capas se pueden colocar en ensamblajes separados dirigidos a .NET Standard. La especificación formal de .NET Standard permite a los desarrolladores crear bibliotecas portátiles que se pueden usar en diversas implementaciones de .NET con un único conjunto unificado de API. Siguiendo estrictamente la arquitectura MVVM y la estrategia de separación de código estándar .NET, podremos usar capas de modelos y modelos de presentación listos para usar al desarrollar la interfaz de usuario para varias plataformas y sistemas operativos.

imagen

Si escribimos una aplicación para el sistema operativo Windows utilizando Windows Presentation Foundation, podemos portarla fácilmente a otros marcos, como, por ejemplo, Avalonia UI o Xamarin Forms, y nuestra aplicación funcionará en plataformas como iOS, Android, Linux, OSX y la interfaz de usuario serán lo único que deberá escribirse desde cero.

Implementación tradicional de MVVM


Los modelos de presentación generalmente incluyen propiedades y comandos a los que se pueden vincular elementos de marcado XAML. Para que los enlaces de datos funcionen, el modelo de vista debe implementar la interfaz INotifyPropertyChanged y publicar el evento PropertyChanged siempre que cambie alguna de las propiedades del modelo de vista. Una implementación simple podría verse así:

public class ViewModel : INotifyPropertyChanged { public ViewModel() => Clear = new Command(() => Name = string.Empty); public ICommand Clear { get; } public string Greeting => $"Hello, {Name}!"; private string name = string.Empty; public string Name { get => name; set { if (name == value) return; name = value; OnPropertyChanged(nameof(Name)); OnPropertyChanged(nameof(Greeting)); } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string name) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } } 

XAML que describe la interfaz de usuario de la aplicación:

 <StackPanel> <TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <TextBlock Text="{Binding Greeting, Mode=OneWay}"/> <Button Content="Clear" Command="{Binding Clear}"/> </StackPanel> 

Y funciona! Cuando el usuario ingresa su nombre en el cuadro de texto, el siguiente texto cambia instantáneamente, saludando al usuario.

Muestra de unión de MVVM

Pero espera un momento! Nuestra interfaz de usuario solo necesita dos propiedades sincronizadas y un comando, ¿por qué necesitamos escribir más de veinte líneas de código para que nuestra aplicación funcione correctamente? ¿Qué sucede si decidimos agregar más propiedades que reflejen el estado de nuestro modelo de vista? El código se hará más grande, el código se volverá más complicado y complicado. ¡Y todavía tenemos que apoyarlo!

Receta # 1. Plantilla de observador. Captadores y colocadores cortos. IU reactiva


De hecho, el problema de la implementación detallada y confusa de la interfaz INotifyPropertyChanged no es nuevo y existen varias soluciones. Lo primero que debe prestar atención a ReactiveUI . Este es un marco MVVM reactivo, funcional y multiplataforma que permite a los desarrolladores de .NET usar extensiones reactivas al desarrollar modelos de presentación.

Las extensiones reactivas son una implementación del patrón de diseño de Observer definido por las interfaces de la biblioteca estándar .NET: IObserver e IObservable. La biblioteca también incluye más de cincuenta operadores que le permiten convertir flujos de eventos (filtrarlos, combinarlos y agruparlos) utilizando una sintaxis similar al lenguaje de consulta estructurado LINQ . Lea más sobre las extensiones de chorro aquí .

ReactiveUI también proporciona una clase base que implementa INotifyPropertyChanged - ReactiveObject. Reescribamos nuestro código de muestra utilizando las características proporcionadas por el marco.

 public class ReactiveViewModel : ReactiveObject { public ReactiveViewModel() { Clear = ReactiveCommand.Create(() => Name = string.Empty); this.WhenAnyValue(x => x.Name) .Select(name => $"Hello, {name}!") .ToProperty(this, x => x.Greeting, out greeting); } public ReactiveCommand Clear { get; } private ObservableAsPropertyHelper<string> greeting; public string Greeting => greeting.Value; private string name = string.Empty; public string Name { get => name; set => this.RaiseAndSetIfChanged(ref name, value); } } 

Tal modelo de presentación hace exactamente lo mismo que el anterior, pero el código que contiene es más pequeño, es más predecible y todas las relaciones entre las propiedades del modelo de presentación se describen en un solo lugar utilizando la sintaxis LINQ to Observable . Por supuesto, podríamos detenernos aquí, pero todavía hay bastante código: tenemos que implementar explícitamente getters, setters y campos.

Receta # 2. Encapsulación INotifyPropertyChanged. Propiedad reactiva


Una solución alternativa es utilizar la biblioteca ReactiveProperty , que proporciona clases de contenedor que son responsables de enviar notificaciones a la interfaz de usuario. Con ReactiveProperty, el modelo de vista no tiene que implementar ninguna interfaz; en cambio, cada propiedad implementa INotifyPropertyChanged. Dichas propiedades reactivas también implementan IObservable, lo que significa que podemos suscribirnos a sus cambios como si estuviéramos usando ReactiveUI . Cambie nuestro modelo de vista usando ReactiveProperty.

 public class ReactivePropertyViewModel { public ReadOnlyReactiveProperty<string> Greeting { get; } public ReactiveProperty<string> Name { get; } public ReactiveCommand Clear { get; } public ReactivePropertyViewModel() { Clear = new ReactiveCommand(); Name = new ReactiveProperty<string>(string.Empty); Clear.Subscribe(() => Name.Value = string.Empty); Greeting = Name .Select(name => $"Hello, {name}!") .ToReadOnlyReactiveProperty(); } } 

Solo necesitamos declarar e inicializar las propiedades reactivas y describir las relaciones entre ellas. No es necesario escribir ningún código repetitivo aparte de los inicializadores de propiedades. Pero este enfoque tiene un inconveniente: debemos cambiar nuestro XAML para que funcionen los enlaces de datos. Las propiedades reactivas son envoltorios, por lo que la interfaz de usuario debe estar vinculada a la propiedad propia de cada envoltorio.

 <StackPanel> <TextBox Text="{Binding Name.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <TextBlock Text="{Binding Greeting.Value, Mode=OneWay}"/> <Button Content="Clear" Command="{Binding Clear}"/> </StackPanel> 


Receta # 3. Cambiar el ensamblaje en tiempo de compilación. PropertyChanged.Fody + ReactiveUI


En un modelo de presentación típico, cada propiedad pública debería poder enviar notificaciones a la interfaz de usuario cuando cambie su valor. Con PropertyChanged.Fody , no tiene que preocuparse por eso. Lo único que se requiere del desarrollador es marcar la clase de modelo de vista con el atributo AddINotifyPropertyChangedInterface , y el código responsable de publicar el evento PropertyChanged se agregará automáticamente a los establecedores después de que se construya el proyecto, junto con la implementación de la interfaz INotifyPropertyChanged, si falta uno. Si es necesario, convierta nuestras propiedades en secuencias de valores cambiantes, siempre podemos usar el método de extensión WhenAnyValue de la biblioteca ReactiveUI . ¡Reescribamos nuestra muestra por tercera vez y veamos cuánto más conciso será nuestro modelo de presentación!

 [AddINotifyPropertyChangedInterface] public class FodyReactiveViewModel { public ReactiveCommand Clear { get; } public string Greeting { get; private set; } public string Name { get; set; } = string.Empty; public FodyReactiveViewModel() { Clear = ReactiveCommand.Create(() => Name = string.Empty); this.WhenAnyValue(x => x.Name) .Select(name => $"Hello, {name}!") .Subscribe(x => Greeting = x); } } 

Fody cambia el código IL del proyecto en tiempo de compilación. El complemento PropertyChanged.Fody busca todas las clases marcadas con el atributo AddINotifyPropertyChangedInterface o implementa la interfaz INotifyPropertyChanged, y edita los establecedores de dichas clases. Puede obtener más información sobre cómo funciona la generación de código y qué otras tareas se pueden resolver del informe de Andrei Kurosh " Reflexión.Emitir. Práctica de uso ".

Aunque PropertyChanged.Fody nos permite escribir código limpio y expresivo, las versiones heredadas de .NET Framework, incluidas 4.5.1 y posteriores, ya no son compatibles. Esto significa que, de hecho, puede intentar usar ReactiveUI y Fody en su proyecto, ¡pero bajo su propio riesgo y teniendo en cuenta que todos los errores encontrados nunca se solucionarán! Las versiones para .NET Core son compatibles de acuerdo con la política de soporte de Microsoft .

De la teoría a la práctica. Validación de formularios con ReactiveUI y PropertyChanged.Fody


Ahora estamos listos para escribir nuestro primer modelo de presentación reactiva. Imaginemos que estamos desarrollando un complejo sistema multiusuario, mientras pensamos en UX y queremos recopilar comentarios de nuestros clientes. Cuando un usuario nos envía un mensaje, necesitamos saber si se trata de un informe de error o una sugerencia para mejorar el sistema, también queremos agrupar las revisiones en categorías. Los usuarios no deben enviar cartas hasta que completen toda la información necesaria correctamente. Un modelo de presentación que satisfaga las condiciones enumeradas anteriormente puede verse así:

 [AddINotifyPropertyChangedInterface] public sealed class FeedbackViewModel { public ReactiveCommand<Unit, Unit> Submit { get; } public bool HasErrors { get; private set; } public string Title { get; set; } = string.Empty; public int TitleLength => Title.Length; public int TitleLengthMax => 15; public string Message { get; set; } = string.Empty; public int MessageLength => Message.Length; public int MessageLengthMax => 30; public int Section { get; set; } public bool Issue { get; set; } public bool Idea { get; set; } public FeedbackViewModel(IService service) { this.WhenAnyValue(x => x.Idea) .Where(selected => selected) .Subscribe(x => Issue = false); this.WhenAnyValue(x => x.Issue) .Where(selected => selected) .Subscribe(x => Idea = false); var valid = this.WhenAnyValue( x => x.Title, x => x.Message, x => x.Issue, x => x.Idea, x => x.Section, (title, message, issue, idea, section) => !string.IsNullOrWhiteSpace(message) && !string.IsNullOrWhiteSpace(title) && (idea || issue) && section >= 0); valid.Subscribe(x => HasErrors = !x); Submit = ReactiveCommand.Create( () => service.Send(Title, Message), valid ); } } 

Marcamos nuestro modelo de vista con el atributo AddINotifyPropertyChangedInterface , por lo que todas las propiedades notificarán a la interfaz de usuario de un cambio en sus valores. Usando el método WhenAnyValue , nos suscribiremos a los cambios en estas propiedades y actualizaremos otras propiedades. El equipo responsable de enviar el formulario permanecerá apagado hasta que el usuario complete el formulario correctamente. Guardaremos nuestro código en la biblioteca de clases dirigida al estándar .NET y pasaremos a las pruebas.

Prueba de modelo de unidad


Las pruebas son una parte importante del proceso de desarrollo de software. Con las pruebas, podremos confiar en nuestro código y dejar de tener miedo de refactorizarlo; después de todo, para verificar el funcionamiento correcto del programa, será suficiente ejecutar las pruebas y asegurarnos de que se completen con éxito. Una aplicación que utiliza la arquitectura MVVM consta de tres capas, dos de las cuales contienen lógica independiente de la plataforma, y ​​podemos probarla utilizando .NET Core y el marco XUnit .

Para crear mobs y stubs , la biblioteca NSubstitute es útil para nosotros, que proporciona una API conveniente para describir las reacciones a las acciones del sistema y los valores devueltos por "objetos falsos".

 var sumService = Substitute.For<ISumService>(); sumService.Sum(2, 2).Returns(4); 

Para mejorar la legibilidad del código y los mensajes de error en nuestras pruebas, utilizamos la biblioteca FluentAssertions . Con él, no solo tendremos que recordar qué argumento en Assert.Equal cuenta el valor real y cuál es el valor esperado, ¡sino que nuestro IDE escribirá el código por nosotros!

 var fibs = fibService.GetFibs(10); fibs.Should().NotBeEmpty("because we've requested ten fibs"); fibs.First().Should().Be(1); 

Escribamos una prueba para nuestro modelo de presentación.

 [Fact] public void ShouldValidateFormAndSendFeedback() { //    , //    . var service = Substitute.For<IService>(); var feedback = new FeedbackViewModel(service); feedback.HasErrors.Should().BeTrue(); //   . feedback.Message = "Message!"; feedback.Title = "Title!"; feedback.Section = 0; feedback.Idea = true; feedback.HasErrors.Should().BeFalse(); //    , //   Send()  IService  //    . feedback.Submit.Execute().Subscribe(); service.Received(1).Send("Title!", "Message!"); } 


IU para la plataforma universal de Windows


Ok, ahora nuestro modelo de presentación está probado y estamos seguros de que todo funciona como se esperaba. El proceso de desarrollo de la capa de presentación de nuestra aplicación es bastante simple: necesitamos crear un nuevo proyecto de Plataforma universal de Windows dependiente de la plataforma y agregar un enlace a la biblioteca .NET Standard que contenga la lógica independiente de la plataforma de nuestra aplicación. A continuación, lo pequeño es declarar los controles en XAML, vincular sus propiedades a las propiedades del modelo de vista y recordar especificar el contexto de datos de cualquier manera conveniente. ¡Hagámoslo!

 <StackPanel Width="300" VerticalAlignment="Center"> <TextBlock Text="Feedback" Style="{StaticResource TitleTextBlockStyle}"/> <TextBox PlaceholderText="Title" MaxLength="{Binding TitleLengthMax}" Text="{Binding Title, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <TextBlock Style="{StaticResource CaptionTextBlockStyle}"> <Run Text="{Binding TitleLength, Mode=OneWay}"/> <Run Text="letters used from"/> <Run Text="{Binding TitleLengthMax}"/> </TextBlock> <TextBox PlaceholderText="Message" MaxLength="{Binding MessageLengthMax}" Text="{Binding Message, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <TextBlock Style="{StaticResource CaptionTextBlockStyle}"> <Run Text="{Binding MessageLength, Mode=OneWay}"/> <Run Text="letters used from"/> <Run Text="{Binding MessageLengthMax}"/> </TextBlock> <ComboBox SelectedIndex="{Binding Section, Mode=TwoWay}"> <ComboBoxItem Content="User Interface"/> <ComboBoxItem Content="Audio"/> <ComboBoxItem Content="Video"/> <ComboBoxItem Content="Voice"/> </ComboBox> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <CheckBox Grid.Column="0" Content="Idea" IsChecked="{Binding Idea, Mode=TwoWay}"/> <CheckBox Grid.Column="1" Content="Issue" IsChecked="{Binding Issue, Mode=TwoWay}"/> </Grid> <TextBlock Visibility="{Binding HasErrors}" Text="Please, fill in all the form fields." Foreground="{ThemeResource AccentBrush}"/> <Button Content="Send Feedback" Command="{Binding Submit}"/> </StackPanel> 

Finalmente, nuestro formulario está listo.

uwp mvvm sample

IU para Xamarin.Forms


Para que la aplicación funcione en dispositivos móviles con sistemas operativos Android e iOS, debe crear un nuevo proyecto Xamarin.Forms y describir la interfaz de usuario utilizando controles Xamarin adaptados para dispositivos móviles.

Muestra mvvm de xamarin.forms

IU para Avalonia


Avalonia es un framework .NET multiplataforma que utiliza el dialecto XAML familiar para los desarrolladores de WPF, UWP o Xamarin.Forms. Avalonia es compatible con Windows, Linux y OSX y está siendo desarrollado por una comunidad de entusiastas de GitHub . Para trabajar con ReactiveUI, debe instalar el paquete Avalonia.ReactiveUI . Describa la capa de presentación en Avalonia XAML!

muestra mvvm de avalonia

Conclusión


Como podemos ver, .NET en 2018 nos permite escribir software verdaderamente multiplataforma : usando UWP, Xamarin.Forms, WPF y AvaloniaUI, podemos proporcionar soporte para nuestros sistemas operativos de aplicaciones Android, iOS, Windows, Linux, OSX. Los patrones y bibliotecas de diseño de MVVM, como ReactiveUI y Fody, pueden simplificar y acelerar el proceso de desarrollo al escribir código claro, fácil de mantener y portátil. La infraestructura desarrollada, la documentación detallada y el buen soporte en los editores de código hacen que la plataforma .NET sea cada vez más atractiva para los desarrolladores de software.

Si está escribiendo aplicaciones de escritorio o móviles en .NET y aún no está familiarizado con ReactiveUI, asegúrese de prestarle atención: el marco utiliza uno de los clientes GitHub más populares para iOS , la extensión Visual Studio para GitHub , el cliente git Atitian SourceTree y Slack para Windows 10 Móvil La serie de artículos sobre ReactiveUI en Habré puede convertirse en un excelente punto de partida. Para los desarrolladores de Xamarin, el curso " Creación de una aplicación iOS con C # " de uno de los autores de ReactiveUI probablemente será útil. Puede obtener más información sobre la experiencia de desarrollo en AvaloniaUI en el artículo sobre Egram , un cliente alternativo para Telegram en .NET Core.

Las fuentes de la aplicación multiplataforma descrita en el artículo y que demuestran las posibilidades de validar formularios con ReactiveUI y Fody se pueden encontrar en GitHub . Un ejemplo de una aplicación multiplataforma que se ejecuta en Windows, Linux, macOS y Android, y que demuestra el uso de ReactiveUI, ReactiveUI.Fody y Akavache también está disponible en GitHub .

Source: https://habr.com/ru/post/es418007/


All Articles