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

