.NET,反应式编程,MVVM模式和代码生成的跨平台开发

响应式MVVM和.NET标准

今天,.NET平台已成为真正的通用工具-借助它,您可以解决各种各样的任务,包括为流行的操作系统(例如Windows,Linux,MacOS,Android和iOS)开发应用程序应用程序。 在本文中,我们将研究使用MVVM设计模式和反应式编程的跨平台.NET应用程序的体系结构。 我们将熟悉ReactiveUIFody库,学习如何使用属性来实现INotifyPropertyChanged接口,接触AvaloniaUIXamarin FormsUniversal Windows PlatformWindows Presentation Foundation.NET Standard的基础知识,并学习用于单元测试模型层和应用程序表示模型的有效工具。

该材料是作者早些时候在Medium资源上发布的文章“ 用于.NET平台的Reactive MVVM ”和“ 通过Reactive MVVM方法的跨平台.NET Apps ”的改编。 示例代码可在GitHub上获得

引言 MVVM体系结构和跨平台.NET


在.NET平台上开发跨平台应用程序时,必须编写可移植且受支持的代码。 如果您使用使用XAML方言的框架(例如UWP,WPF,Xamarin Forms和AvaloniaUI),则可以使用MVVM设计模式,反应式编程和.NET Standard代码分离策略来实现。 这种方法通过允许开发人员在各种操作系统上使用通用代码库和通用软件库,提高了应用程序的可移植性。

我们将仔细研究基于MVVM架构构建的应用程序的每一层-模型,视图和视图模型。 模型层表示域服务,数据传输对象,数据库实体,存储库-我们程序的所有业务逻辑。 该视图负责在屏幕上显示用户界面元素,并取决于特定的操作系统,并且表示模型允许上述两个层进行交互,从而使模型层与人类用户进行交互。

MVVM体系结构提供了应用程序三个软件层之间的职责划分,因此可以将这些层放在针对.NET Standard的单独程序集中。 正式的.NET Standard规范允许开发人员创建可移植的库,这些库可以通过一组统一的API在各种.NET实现中使用。 严格遵循MVVM体系结构和.NET Standard代码分离策略,在为各种平台和操作系统开发用户界面时,我们将能够使用现成的模型层和表示模型。

图片

如果我们使用Windows Presentation Foundation为Windows操作系统编写了一个应用程序,则可以轻松地将其移植到其他框架,例如Avalonia UI或Xamarin Forms,并且我们的应用程序将可以在iOS,Android, Linux,OSX和用户界面将是唯一需要从头开始编写的东西。

传统MVVM实施


表示模型通常包括可以绑定XAML标记元素的属性和命令。 为了使数据绑定起作用,视图模型必须实现INotifyPropertyChanged接口,并在视图模型的任何属性发生更改时发布PropertyChanged事件。 一个简单的实现可能看起来像这样:

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描述了应用程序UI:

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

而且有效! 当用户在文本框中输入他的名字时,下面的文本立即更改,向用户打招呼。

MVVM绑定样本

但请稍等! 我们的UI仅需要两个同步属性和一个命令,为什么我们需要编写二十多行代码才能使应用程序正常工作? 如果我们决定添加更多反映视图模型状态的属性,会发生什么? 代码将变得更大,代码将变得越来越复杂。 而且我们仍然必须支持他!

1号配方 观察者模板。 简短的获取器和设置器。 反应式UI


实际上,INotifyPropertyChanged接口的冗长而令人困惑的实现问题并不新鲜,有几种解决方案。 您首先要注意ReactiveUI 。 这是一个跨平台,功能性的反应式MVVM框架,允许.NET开发人员在开发表示模型时使用反应式扩展。

反应性扩展是由标准.NET库-IObserver和IObservable的接口定义的Observer设计模式的实现。 该库还包括五十多个运算符,这些运算符使您可以使用类似于LINQ结构化查询语言的语法来转换事件流-对其进行过滤,合并和分组。 在此处阅读有关喷气机扩展的更多信息。

ReactiveUI还提供实现INotifyPropertyChanged-ReactiveObject的基类。 让我们使用框架提供的功能来重写示例代码。

 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); } } 

这样的表示模型与上一个模型完全相同,但是其中的代码更小,更可预测,并且使用LINQ to Observable语法在一处描述了表示模型​​的属性之间的所有关系。 当然,我们可以在这里停止,但是仍然有很多代码-我们必须显式实现getter,setter和field。

2号配方 封装INotifyPropertyChanged。 反应性


一种替代解决方案是使用ReactiveProperty库,该库提供负责将通知发送到用户界面的包装器类。 使用ReactiveProperty,视图模型不必实现任何接口;相反,每个属性本身都实现INotifyPropertyChanged。 这样的反应性属性也实现了IObservable,这意味着我们可以像使用ReactiveUI一样订阅它们的更改。 使用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(); } } 

我们只需要声明和初始化反应特性并描述它们之间的关系。 除了属性初始化程序外,无需编写任何样板代码。 但是这种方法有一个缺点-我们必须更改XAML才能使数据绑定起作用。 反应性属性是包装器,因此UI必须绑定到每个此类包装器的自身属性!

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


配方3。 在编译时更改程序集。 PropertyChanged.Fody + ReactiveUI


在典型的表示模型中,每个公共属性的值更改时都应该能够将通知发送到用户界面。 使用PropertyChanged.Fody ,您不必担心。 开发人员唯一需要做的就是用AddINotifyPropertyChangedInterface属性标记视图模型类-负责发布PropertyChanged事件的代码将在项目构建后与INotifyPropertyChanged接口的实现一起自动添加到设置器中(如果缺少)。 如有必要,将我们的属性转换为不断变化的值,我们始终可以使用ReactiveUI库中的WhenAnyValue扩展方法 。 让我们第三次重写示例,看看我们的演示模型将更加简洁!

 [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在编译时更改项目IL代码。 PropertyChanged.Fody附加组件搜索标记有AddINotifyPropertyChangedInterface属性或实现INotifyPropertyChanged接口的所有类,并编辑此类的设置器。 您可以从Andrei Kurosh的报告“ Reflection.Emit。Use of Practice ”中了解有关代码生成如何工作以及可以解决什么其他任务的更多信息。

尽管PropertyChanged.Fody允许我们编写整洁且富有表现力的代码,但不再支持.NET Framework的旧版本,包括4.5.1和更高版本。 实际上,这意味着您可以尝试在项目中使用ReactiveUI和Fody,但是要自担风险,并且要考虑到发现的所有错误将永远不会修复! 根据Microsoft支持策略,支持.NET Core的版本。

从理论到实践。 使用ReactiveUI和PropertyChanged.Fody验证表单


现在,我们准备编写我们的第一个反应式表示模型。 假设我们正在开发一个复杂的多用户系统,同时考虑UX,并希望收集客户的反馈。 当用户向我们发送消息时,我们需要知道它是错误报告还是改进系统的建议,我们还希望将评论分为几类。 在正确填写所有必要信息之前,用户不应发送信件。 满足上述条件的演示模型可能如下所示:

 [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 ); } } 

我们使用AddINotifyPropertyChangedInterface属性标记视图模型-因此,所有属性都将向UI通知其值的更改。 使用WhenAnyValue方法,我们将订阅对这些属性的更改,并将更新其他属性。 负责提交表单的团队将一直保留到用户正确填写表单为止。 我们将代码保存在针对.NET Standard的类库中,然后继续进行测试。

单元模型测试


测试是软件开发过程的重要组成部分。 通过测试,我们将能够信任我们的代码,而不必害怕重构它-毕竟,要检查程序的正确操作,就足以运行测试并确保其成功完成。 使用MVVM体系结构的应用程序由三层组成,其中两层包含平台无关的逻辑-我们可以使用.NET Core和XUnit框架对其进行测试。

要创建生物存根NSubstitute库对我们很有用,它提供了一个方便的API,用于描述对系统操作的反应以及“假对象”返回的值。

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

为了提高测试中代码和错误消息的可读性,我们使用FluentAssertions库。 有了它,我们不仅要记住Assert.Equal中的哪个参数会计算实际值,而且哪个是期望值,而且我们的IDE会为我们编写代码!

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

让我们为演示模型编写一个测试。

 [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!"); } 


Windows Universal Platform用户界面


好的,现在我们的演示模型已经过测试,我们可以确保一切正常。 开发应用程序表示层的过程非常简单-我们需要创建一个新的依赖于平台的通用Windows平台项目,并添加一个指向包含应用程序独立于平台的逻辑的.NET标准库的链接。 接下来,小事情是在XAML中声明控件,将其属性绑定到视图模型的属性,并记住以任何方便的方式指定数据上下文 。 开始吧!

 <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> 

最后,我们的表格已经准备好了。

uwp mvvm示例

Xamarin.Forms的UI


为了使该应用程序在运行Android和iOS操作系统的移动设备上运行,您需要创建一个新的Xamarin.Forms项目并使用适用于移动设备的Xamarin控件描述UI

xamarin.forms mvvm示例

Avalonia用户界面


Avalonia是.NET的跨平台框架,它使用WPF,UWP或Xamarin.Forms开发人员熟悉的XAML方言。 Avalonia支持Windows,Linux和OSX,并由GitHub上的一群爱好者开发。 要使用ReactiveUI,必须安装Avalonia.ReactiveUI软件包。 描述 Avalonia XAML上的表示层

Avalonia mvvm样本

结论


如我们所见,.NET在2018年允许我们编写真正的跨平台软件 -使用UWP,Xamarin.Forms,WPF和AvaloniaUI,我们可以为我们的应用程序操作系统Android,iOS,Windows,Linux,OSX提供支持。 MVVM设计模式和库(例如ReactiveUIFody)可以通过编写清晰,可维护和可移植的代码来简化和加快开发过程。 开发的基础结构,详细的文档以及代码编辑器的良好支持,使.NET平台对软件开发人员越来越有吸引力。

如果您正在用.NET编写桌面或移动应用程序,但还不熟悉ReactiveUI,请务必注意-该框架使用的是iOS上最受欢迎的GitHub客户端,GitHub的 Visual Studio扩展,git客户端Atlassian SourceTreeWindows 10的Slack。手机版 有关Habré上ReactiveUI的系列文章可以成为一个很好的起点。 对于Xamarin上的开发人员而言,ReactiveUI的一位作者撰写的“ 使用C#构建iOS应用 ”课程可能会派上用场。 您可以从有关Egram (.NET Core上Telegram的替代客户端) 的文章中了解有关AvaloniaUI开发经验的更多信息

可以在GitHub上找到本文中描述的跨平台应用程序的源代码,并演示了使用ReactiveUI和Fody验证表单的可能性。 GitHub上还提供了一个跨平台应用程序示例,该示例在Windows,Linux,macOS和Android上运行,并演示了ReactiveUI,ReactiveUI.Fody和Akavache的用法

Source: https://habr.com/ru/post/zh-CN418007/


All Articles