Xamarin.Forms MVVM 与 XAML(二) 电脑版发表于:2022/4/2 17:23  >#Xamarin.Forms MVVM 与 XAML(二) [TOC] ##MVVM简介 tn2>MVVM是Model-View-View-Model的简写。它与常常使用的MVC有些相似。Model表示你的数据,View表示你的用户视图界面。通常我们是使用的XMAL来构建的MVVM的视图。 >### View Model tn2>ViewModel就像你应用程序中的核心一样,它将参与各种Web服务或为你的页面执行任何应用程序逻辑的东西,它也是使一切正常工作的原因。它由几个属性,它们绑定到视图上的UI控件。ViewModel包含所有由UI特定的接口和属性,并由一个 ViewModel 的视图的绑定属性,并可获得二者之间的松散耦合,所以需要在ViewModel 直接更新视图中编写相应代码。 例如:你有一个按钮,一个按钮有一个命令属性,所以当用户点击它时,命令动作会被触发,因此视图模型具有绑定到UI显示。反之,它也可以通过命令对用户对它的操作做出反应。  ## 实验项目 tn2>结合上篇文章的项目来继续我们的代码编写,可在此处参考<a href="https://www.tnblog.net/hb/article/details/7143">上一篇文章</a>。 >### 实验目的 tn2>我们希望在上一个项目的基础之上对Editor编辑器控件实现MVVM的交互。 >### 代码编写 tn2>首先创建一个`MainPageViewModel`的类并且实现`INotifyPropertyChanged`接口,该接口可以通过`PropertyChanged`事件属性来提醒前端视图属性已经更新了。 `AllNotes`是存储Editor所保存文本内容的集合。 `SaveCommand`是保存按钮的触发命令,先将内容保存至`AllNotes`后,再清空Editor中的内容。 `EraseCommand`是清空按钮的命令,主要是清空Editor中的文本内容。 ```csharp public class MainPageViewModel : INotifyPropertyChanged { public MainPageViewModel() { EraseCommand = new Command(() => { TheNote = string.Empty; }); SaveCommand = new Command(() => { AllNotes.Add(TheNote); TheNote = string.Empty; }); } public ObservableCollection<string> AllNotes { get; set; } = new ObservableCollection<string>(); public event PropertyChangedEventHandler PropertyChanged; string theNote; public string TheNote { get => theNote; set { theNote = value; var args = new PropertyChangedEventArgs(nameof(TheNote)); PropertyChanged?.Invoke(this, args); } } public Command SaveCommand { get; } public Command EraseCommand { get; } } ``` tn2>除此之外在UI中的`MainPage.xaml`中的用户界面需要进行一定的更改。 在`ContentPage`中我们首先需要通过`xmlns:local`属性来引用我们的命名空间,一般格式如下,这里我们引用刚写好的`MainPageViewModel`类所在的命名空间。 ```bash xmlns:local="clr-namespace:所引用资源的完整命名空间" ``` tn2>随后通过`ContentPage.BindingContext`标签绑定`MainPageViewModel`到数据上下文中去。 ```xml <ContentPage.BindingContext> <local:MainPageViewModel/> </ContentPage.BindingContext> ``` tn2>将Lable标签删除掉,添加上`CollectionView`集合可视标签,需要注意的是它需要通过`ItemsSource`属性来绑定数据源,这里我们绑定的`AllNotes`Editor中的文本内容集合,格式为:`{Binding [属性名]}`(注意必须是公开的属性)。 在这之下我们还需要定义`CollectionView.ItemTemplate`标签来定义每个Editor中的内容所呈现的模板。 关于CollectionView包括定义要显示的数据及其外观的以下属性: | 属性名 | 类型 | 描述 | | ------------ | ------------ | ------------ | | `ItemsSource` | IEnumerable | 指定要显示的项目的集合,默认值为null。 | | `ItemTemplate` | DataTemplate | 指定要应用于要显示的项目集合中的每个项目的模板。 | tn2>由于`ItemTemplate`是`DataTemplate`类型所以需要在`ItemTemplate`之下定义该标签。然后我们可以定义`StackLayout`标签,像堆栈式的集合一样放入我们的内容,然后通过`Label`标签绑定的我们的内容,设置大小为`Title`,并在`Label`外面嵌入一层`Frame`。目前可以把`Frame`标签想成我们前端使用的`div`,可以进行`padding`、`BorderColor`等调整,后面我们还会讲解到。代码如下所示: ```xml <CollectionView ItemsSource="{Binding AllNotes}" Grid.Row="3" Grid.ColumnSpan="2"> <CollectionView.ItemTemplate> <DataTemplate> <StackLayout> <Frame> <Label Text="{Binding .}" FontSize="Title"/> </Frame> </StackLayout> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView> ``` tn2>对了,最重要的我们还需要将`TheNote`绑定至`Editor`编辑器中。 ```xml <Editor Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="1" Placeholder="Enter Note Here" Text="{Binding TheNote}" /> ``` tn2>完整的代码如下所示: ```xml <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:FirstApp" x:Class="FirstApp.MainPage"> <ContentPage.BindingContext> <local:MainPageViewModel/> </ContentPage.BindingContext> <!--创建一个画布--> <Grid> <!--行占4份--> <Grid.RowDefinitions> <!-- Height 行高 --> <RowDefinition Height="*"/> <RowDefinition Height="2*"/> <RowDefinition Height=".5*"/> <RowDefinition Height="2*"/> </Grid.RowDefinitions> <!--列占2份--> <Grid.ColumnDefinitions> <!-- Width 列宽 --> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <!--添加一个图片元素--> <!-- BackgroundColor:背景颜色(深蓝色) Grid.Row:图片占的哪一行 Grid.Column:图片占的哪一列 Grid.ColumnSpan:按照列占领几个(1个或2个) --> <Image Source="logo_xamarin" BackgroundColor="PowderBlue" Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" /> <!--添加一个编辑器--> <!-- Placeholder:默认显示文本。 --> <Editor Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="1" Placeholder="Enter Note Here" Text="{Binding TheNote}" /> <!--添加两个按钮--> <!-- Text:文本内容 --> <Button Grid.Row="2" Grid.Column="0" Text="Save" Command="{Binding SaveCommand}" /> <Button Grid.Row="2" Grid.Column="1" Text="Erase" Command="{Binding EraseCommand}" /> <CollectionView ItemsSource="{Binding AllNotes}" Grid.Row="3" Grid.ColumnSpan="2"> <CollectionView.ItemTemplate> <DataTemplate> <StackLayout> <Frame> <Label Text="{Binding .}" FontSize="Title"/> </Frame> </StackLayout> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView> </Grid> </ContentPage> ``` >### 展示案例效果   ##Command 的介绍 tn2>简而言之,Command就是响应用户 键盘快捷键输入或者控件事件 (如button点击, 工具条,菜单栏等等), 从而完成如复制、黏贴、打印等操作的一个过程。 >### Command 内部结构  tn2>`Commands`本身什么也不做,它底层由`ICommand`组成,包含两个方法(`Execute`/`CanExecute`)和一个事件(`CanExecuteChanged`).要执行实际的action,需要将command和你的代码关联起来,这就是`Command bindings`的作用. 关于其中方法与属性的作用,如下表所示。 | 属性与方法 | 描述 | | ------------ | ------------ | | `_canExecute` | 表示该命令是否生效的委托。 | | `_execute` | 表示传入需要执行命令的委托。 | | `_weakEventManager` | WeakEventManager一种弱事件的管理器,其实内部是由一种键值对的方式存储事件。(后面还会讲到) | | `CanExecuteChanged` | 这个方法表示判断该命令是否有效时之前所触发的事件,该事件是由WeakEventManager来管理的,键名为:CanExecuteChanged | | `CanExecute` | 判断该命令是否有效,返回bool值 | | `Execute` | 立即调用该命令 | | `ChangeCanExecute` | 处理命令是否生效前所调用的方法 | tn2>关于构造,第一个参数表示我们要执行的命令代码,第二个参数表示该命令是否可以执行。 举例:当我们在`EraseCommand`命令中传入第二个委托(也就是`_canExecute`),返回值为`false`它将不会执行该命令。 (如下图所示根本点都点不了) ```csharp EraseCommand = new Command(() => { TheNote = string.Empty; },()=> { return false; }); ```  >###扩展:讲讲WeakEventManager tn2>平时我们用事件的时候呢,用得不好会导致内存泄漏。 举例:如下面的代码所示。 ```csharp public class FootEventArgs: EventArgs { } public class FootManager { public event EventHandler<FootEventArgs> FootSignalChanged; } ``` ```csharp public class MyClass { public FootManager _footManager; public MyClass(FootManager footManager) { footManager.FootSignalChanged += OnFootChanged; _footManager = footManager; } private void OnFootChanged(object sender, FootEventArgs e) { // 你的代码 } } ``` ```csharp public void DoMaster(FootManager footManager) { var myClass = new MyClass(footManager); myClass.DoSomething(); } ``` tn2>如果`FootManager`这个对象它与应用程序的生命周期一样,也就是与应用程序同生共死。再执行`DoMaster`方法后,`MyClass`的一个实例被创建并且不再使用。但我们的GC并不会收集它,因为`FootManager`中的事件`FootSignalChanged`有对`MyClass`中的`OnFootChanged`方法有所引用,所以会导致我们的**内存泄漏**。而且GC永远不会收集`MyClass`。 普通的处理方法是:实现IDisposable接口并取消引用的事件。 ```csharp public class MyClass: IDisposable { public FootManager _footManager; public MyClass(FootManager footManager) { footManager.FootSignalChanged += OnFootChanged; _footManager = footManager; } private void OnFootChanged(object sender, FootEventArgs e) { // 你的代码 } public void Dispose() { _footManager.FootSignalChanged -= OnFootChanged; } } ``` tn2>当然还可以写个方法取消该事件的引用,也是可以的。接下来我们讲讲弱事件。 tn> 弱事件任然可以完美的解决这个问题。 tn2>前面存在内存泄漏的时候,应用程序告诉GC**这是必需品你不可以回收**. 弱引用/弱事件表示,应用程序告诉GC**我不太需要它,如果我在使用你不要回收,如果没用了你可以随意回收** 它是使用 .NET 的`WeakReference`类实现的,也被我们称为事件聚合器。 如下代码所示便可以解决内存泄漏的问题。 ```csharp public class MyClass { public MyClass(FootManager footManager) { footManager.FootSignalChanged += new WeakEventHandler<FootManager>(OnFootChanged).Handler;; } private void OnFootChanged(object sender, WifiEventArgs e) { // 你的代码 } } ``` tn2>而我们的`WeakEventManager`就是弱引用处理程序的实现之一。WPF 使用类内置了对侦听器端弱事件的支持`WeakEventManager`。它的工作方式类似于以前的包装器解决方案,不同之处在于单个`WeakEventManager`实例充当多个发送者和多个侦听器之间的包装器。由于这个单一实例,`WeakEventManager`当事件从不被调用时,可以避免泄漏:在 a 上注册另一个事件WeakEventManager可以触发对旧事件的清理。这些清理是使用 WPF 调度程序安排的,它们只会发生在运行 WPF 消息循环的线程上。  tn>简单来说就是一个单例,将事件注册到了键值对中,然后每一次调用事件时都会去检查软引用有没有,如果没有将会被清理。 Key:事件名 Value.Subscriber.Target :可获取当前委托在其上调用实例方法的类实例。(`Value.Subscriber.Target`) Value.Handler :事件调用的方法。(`Value.Handler`) tn2>此外,它WeakEventManager还有一个我们以前的解决方案没有的限制:它需要正确设置 sender 参数。如果您使用它附加到button.Click,则只会传递带有的事件sender==button。一些事件实现可能只是将处理程序附加到另一个事件: ```csharp public event EventHandler Event { add { anotherObject.Event += value; } remove { anotherObject.Event -= value; } } ``` tn2>此类事件不能与 一起使用WeakEventManager。 每个事件有一个`WeakEventManager`类,每个线程都有一个实例。定义这些事件的推荐模式是大量样板代码。 幸运的是,我们可以使用泛型来简化它: ```csharp public sealed class ButtonClickEventManager : WeakEventManagerBase<ButtonClickEventManager, Button> { protected override void StartListening(Button source) { source.Click += DeliverEvent; } protected override void StopListening(Button source) { source.Click -= DeliverEvent; } } ``` tn>注意DeliverEvent需要(object, EventArgs),而Click事件提供(object, RoutedEventArgs)。虽然委托类型之间没有转换,但 C#在从方法组创建委托时支持逆变。 tn2>文献:https://www.codeproject.com/Articles/29922/Weak-Events-in-C ## ObservableCollection源码分析  tn2>ObservableCollection是一个集合类,继承`Collection`集合,并且实现了`INotifyCollectionChanged`, `INotifyPropertyChanged`也就是属性通知与集合通知。继承的类与接口意义如下 | 类名 | 描述 | | ------------ | ------------ | | `Collection` | 为泛型集合提供基类。 | | `INotifyCollectionChanged` | 将集合的动态更改通知给侦听器,例如,何时添加和移除项或者重置整个集合对象。 | | `INotifyPropertyChanged` | 向客户端发出某一属性值已更改的通知。 | tn2>所以再ObservableCollection这个类的方法,对数据的操作很少,重点放在了当自己本事变化的时候(不管是属性,还是集合)会调用发出通知的事件。一般用于更新UI。 >### Add方法源码分析 tn2>我们来看看当我们添加一个元素,时会发生什么事情。 首先我们调用Add方法时会调用`Collection`父类的方法。  tn2>但是它在Add方法中调用了InsertItem方法,并且对该方法进行了重写。  tn2>通过ObservableCollection发出添加通知集合事件与属性事件来更新UI,这样的集合我们称为动态数据集合。 >### ObservableCollection与List的关系 tn2>其实在ObservableCollection就是在List的基础上多添加了事件通知,因为在ObservableCollection类中操作元素的仍然是List集合。  ## PropertyChangedEventHandler事件处理 tn2>MainPageViewModel类实现了INotifyPropertyChanged接口,同时也实现了属性通知的事件`PropertyChangedEventHandler PropertyChanged`。我们重新赋值`TheNote`时,我们发送了一个更新`TheNote`属性的一个事件通知,此时用户界面将会更新绑定`TheNote`属性的控件。反之我们去掉这段前端将没有任何反应。 下图将展示去掉该代码后,并没有清空前端编辑框。 ```csharp var args = new PropertyChangedEventArgs(nameof(TheNote)); PropertyChanged?.Invoke(this, args); ``` 