在Xamarin.Forms中使用MVVM设计模式

英文原文地址:MVVM

Xamarin.Forms的开发者经常会在XAML文件中创作用UI,并在其后面添加UI的操作代码。随着应用的更改、体积增大和覆盖面越来越广,就会出现复杂的维护问题。譬如UI控件和业务逻辑紧密耦合,这会提高修改UI的成本,也会提升代码单元测试难度。

Model-View-ViewModel(MVVM)模式有助于更为清晰分开业务和应用的UI展示逻辑。保持应用逻辑与UI的清晰分离有助于解决很多开发问题,也会让应用更易于测试、维护和演进。同时也会极大提升代码复用率,并使开发者和UI设计师更好地合作,并各司其职。

MVVM模式

在MVVM模式中有三个核心模块:model、view和view model。每个模块目的不同。下图展示了三个模块之间的相互关系。

MVVM设计模式
MVVM设计模式

除了需要理解每个模块的责任,理解它们之间如何交互的也同样重要。总体来说,view“知道”view model,view model“知道”model,但是model不知道view model,view model也不了解view。因此,view model将view和model分割开来,并且允许model独立于view演进。

使用MVVM模式的好处有:

  • 如果现在已经有了封装了业务逻辑的model实现,那么很难或者说更改它会有很大风险。在这种场景下,view model就像model类的适配器,从而使你不用大幅度更改model代码。
  • 开发者可以在不用view的情况下创建view model和model的单元测试。在view model上使用的单元测试与在view中使用是同样的功能效果。
  • 如果UI全部使用XAML实现,那么就可以在不更改代码的基础上就可以修改应用UI。因此,新版的view需要匹配现有的view model。
  • 在开发过程中,设计师和开发者可以独立同步进行组件的工作。设计师可以集中精力到view上,开发者则只需关注view model和model组件。

有效使用MVVM的关键在于理解如何把应用代码合理分解成合适的类中,并明白这些类之间如何交互。接下来的部分将讨论MVVM中每个类的责任。

View

View负责定义用户所能看到的组成、布局和外观。理想情况下,每个view都在XAML文件中定义,后面只有有限的、不包含业务逻辑的代码。然而,在一些情况下,后面的代码也许会包含一些在XAML中很难表现出来的视觉效果的实现,譬如动画。

在Xamarin.Forms应用中,view通常是一个继承于Page或者ContentView的类。然而,view也可以通过data template表现,这种情况下,显示的UI元素就是用来表现一个对象。Data template作为view的时候后面并不包含代码,其被设计成需要绑定特定的view model。

💡提示

不要在后置代码中更改UI元素的激活状态。确保view model负责定义影像view显示方面的逻辑状态变化,譬如一个命令是否可用,或者一个操作正在挂起中的提示。因此,通过绑定view model的属性来更改UI元素的激活状态,而不要在后置的代码中直接更改。

调用与view交互(如点击按键或选择项目)对应的view model中的代码有几种办法。如果控件支持命令(command),控件的Command属性可以数据绑定到view model中的ICommand属性中。当控件的命令被调用的时候,会执行view model中对应的代码。除了command,表现(behavior)也可以绑定到view model中的对象里面,并监听调用的command或者发起的事件(event)。Behavior可以接着调用view model中的ICommand或者一个方法作为回应。

ViewModel

View model实现view数据绑定需要的属性和命令,并通过变更通知事件的方式来提示view其状态的变化。View model提供的属性和命令定义了UI所提供的功能,但是由UI决定这些功能如何显示。

💡提示

通过异步操作符防止UI卡死。不要阻塞移动应用的UI线程,这样才能提升用户体验。因此,在view model中,对于读写(I/O)操作使用异步方法,并且异步地发起event来通知view其属性的变化。

View model同时也要负责匹配view与任何需要的model类之间的交互。通常view model和view类之间时一对多的关系。View model可以选择把model类直接暴露给view,从而使view中的控件可以直接数据绑定到model上。在这种情况下,需要把model类设计成支持数据绑定和变化通知事件的。

每个view mode从model中为view提供可以方便使用的数据。为了实现这个目的,view model有时候会进行数据转化。在view model中进行数据转化不失为一个好主意,因为这为view提供了可供其绑定的属性。举例来说,view model可能会降两个属性值合并在在一起,从而使view更便于显示。

💡提示

把数据转化集中在转化层中。也可以在view model与view之间将转化器作为独立的数据转化层使用。比如,当数据需要特殊的格式但在view model中并没有提供的时候,这种方式就很有用。

为了让view model参与到view的双向(two-way)绑定中去,view model的属性必须触发PropertyChanged事件。通过实现INotifyPropertyChanged接口来实现这个功能,并在一个属性改变的时候出发PropertyChanged事件。

对于集合(collection),提供了对view友好的ObservableCollection<T>。这个集合实现了集合改变时候的通知,从而让开发者不必对集合自己去实现INotifyCollectionChanged接口。

Model

Model类是封装应用数据等不可见的类。因此可以认为,model代表了应用的域内model,通常包含数据模型,同时也有业务和数据验证逻辑。比如model对象常见的有数据传输对象(data transfer obejects,DTOs)、简单传统CLR对象(Plain Old CLR Objects,POCOs),以及生成的实体(entity)和代理(proxy)对象。

Model类通常与封装数据存取和缓存的服务或源一起用。

连接view model和view

可利用Xamarin.Forms提供的数据绑定功能来连接view model和view。构建view和view model并关联二者的方式有很多种。这些方式分为两种,一种是view优先构成(view first composition),另一种是view model优先构成(view model first composition)。选择哪个,取决于个人偏好和复杂度。然而,所有的方式都为着同一个目标,就是对view来说需要为其BindingContext分配一个view model。

对于view优先构成来说,概念上,应用由一些连接到其所依赖的view model上的view构成。采用这种方式的好处是,这样可以轻松构建低耦合、易于单元测试的应用。因为view model并不依赖于view。通过其视觉结构就可以很容易地理解应用结构,而无需遍历代码去理解各种类是如何创建和联系在一起的。此外,view优先构成与Xamarin.Forms的负责导航并构建页面的导航系统一致,这也使得view model优先构成更为复杂,与平台不匹配。

使用view model优先构成,应用概念上由view model组成,并配以负责定位view model对应view的服务。由于view的创建被抽象掉了,可以开发者关注于应用的非UI逻辑结构,所以一些开发者对于这种方式感到更为自然。可是,这个方式通常更为复杂,也更难于理解应用不同部分如何创建和关联的。

💡提示

让view model和view相互独立。把view绑定到一个数据源应该让view与其对应的view model各自独立。具体来说,在view model中不要引用view的类型,如ButtonListView。遵循上面所说的原则,view model可以独立测试,因此减少了由于有限规模所带来瑕疵的可能性。

接下来讨论把view model连接到view的方法。

显式创建一个view model

最简单的方式就是在view的XAML文件中显式实例化其对应的view model。当view被创建的时候,对应的view model对象也会被创建。如下所示:

<ContentPage ... xmlns:local="clr-namespace:eShop">  
    <ContentPage.BindingContext>  
        <local:LoginViewModel />  
    </ContentPage.BindingContext>  
    ...  
</ContentPage>

ContentPage创建的时候,LoginViewModel的实例会被自动创建,并设置成view的BindingContext

这种显式构建并分配view model的方式优点是简单,但是需要view model由一个默认(无参数)构造函数。

代码创建view model

在后置的代码文件中可以把view model分配给view中的BindingContext属性。通常这在view的构造函数中完成,如下所示:

public LoginView()  
{  
    InitializeComponent();  
    BindingContext = new LoginViewModel(navigationService);  
}

在view的后置代码中构建并赋值view model的好处是简单。但是,这个方式的主要缺点是view需要提供view model所有需要的依赖。使用依赖注入有助于维护view和view model之间的低耦合。更多信息参见依赖注入

创建定义为data template的view

可以把view定义为data template并将其与一个view model类型关联。可将data template定义为资源,或者在显示view model的控件内部定义。在移动应用eShopOnContainers中,ViewModelLocator类有一个关联的属性AutoWireViewModel,用来把view model与view关联起来。在view的XAML文件中,这个关联的属性被设置为true,从而表示view model应该自动与view关联,如下所示:

viewModelBase:ViewModelLocator.AutoWireViewModel="true"

AutoWireViewModel是一个可绑定属性,初始值为false,当其值改变的时候,调用OnAutoWireViewModelChanged事件。这个方式为view决定了view model。下面代码展示了如何实现的:

private static void OnAutoWireViewModelChanged(BindableObject bindable, object oldValue, object newValue)  
{  
    var view = bindable as Element;  
    if (view == null)  
    {  
        return;  
    }  

    var viewType = view.GetType();  
    var viewName = viewType.FullName.Replace(".Views.", ".ViewModels.");  
    var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;  
    var viewModelName = string.Format(  
        CultureInfo.InvariantCulture, "{0}Model, {1}", viewName, viewAssemblyName);  

    var viewModelType = Type.GetType(viewModelName);  
    if (viewModelType == null)  
    {  
        return;  
    }  
    var viewModel = _container.Resolve(viewModelType);  
    view.BindingContext = viewModel;  
}

OnAutoWireViewModelChanged方法试图用通用的方式处理view model。这个通用的方式假设:

  • View model和view类型在同一个assembly中。
  • View在一个.Views的子命名空间中。
  • View model在一个.ViewModels的子命名空间中。
  • View model名称与view对应,且结尾是“ViewModel”。

最后,OnAutoWireViewModelChanged方法设置view类型BindingContext来决定view model类型。欲了解更多决定view model类型的信息,参见Resolution

使用这种方法可以让应用有一个单独的负责初始化view model和关联view的类。

💡提示

为了便于替代,使用view model的(定位器)locator。也可将View model的定位器用作替换依赖实现的替换点,譬如单元测试或者设计时间数据的时候。

根据view model或model的改变更新view

所有view可获取到的view model和model类都应该实现INotifyPropertyChanged接口。在view model或者model类中实现这个接口可以让其数据绑定到的view中的任何控件在其值改变时接收到通知。

应用需要为正确接受属性变化通知而设计,需要遵循以下几点:

  • 如果一个public的属性变化,要触发PropertyChanged事件(event)。不要因为了解XMAL绑定如何发生的就忽略PropertyChanged事件。
  • 任何在其他view model或view中使用的计算属性(calculated property)都要触发PropertyChanged事件。
  • 在改变属性方法的最后,或者当认为一个对象在安全状态的时候,都要触发PropertyChanged事件。通过同步地调用事件的处理器(handler),触发事件会打断运行。如果这是在运行过程中出现的,当对象处于不安全、部分更新状态的时候,这会将其暴露给回调函数。此外,也可以让PropertyChanged事件触发多重变化。在多重变化安全运行之前,需要更新完成。
  • 如果属性值不改变,永远不要触发PropertyChanged事件。这意味着,在触发PropertyChanged事件之前,必须要比较新旧数据值。
  • 如果在view model的构造函数中初始化属性值的时候,不要触发PropertyChanged事件。在这个阶段,view中有数据绑定的控件还没有订阅来接受变化的通知。
  • 在一个类的一个public的方法的单同步调用中,在同样的属性名称参数下,不要触发超过一次PropertyChanged事件。举例来说,假设NumberOfItems属性背后是_numberOfItems域来存储,如果一个方法在训话中增加了50次_numberOfItems,在所有过程结束之后,则应该仅触发一次NumberOfItems属性的变化通知。对于异步方法来说,在异步继续链的每个同步模块的给定属性名称上触发PropertyChanged事件。

eShopOnContainers应用使用ExtendedBindableObject类来提供变化通知,如下所示:

public abstract class ExtendedBindableObject : BindableObject  
{  
    public void RaisePropertyChanged<T>(Expression<Func<T>> property)  
    {  
        var name = GetMemberInfo(property).Name;  
        OnPropertyChanged(name);  
    }  

    private MemberInfo GetMemberInfo(Expression expression)  
    {  
        ...  
    }  
}

Xamarin.Forms的BindableObject类实现了INotifyPropertyChanged接口,并提供了OnPropertyChanged方法。ExtendedBindableObject类提供了用来激发属性变化通知的RaisePropertyChanged方法,并利用了BindableObject类中所提供的功能来实现。

eShopOnContainers应用中的每个view model类都继承于ViewModelBase类,ViewModelBase类又继承于ExtendedBindableObject类。因此,每个view model类都适用了ExtendedBindableObject类中RaisePropertyChanged方法来提供属性改变通知。下面代码展示了eShopOnContainers如何通过lambda表达式激发属性变化通知:

public bool IsLogin  
{  
    get  
    {  
        return _isLogin;  
    }  
    set  
    {  
        _isLogin = value;  
        RaisePropertyChanged(() => IsLogin);  
    }  
}

注意,这样使用lambda表达式会带来略微的性能损失,因为每次调用都会检查lambda表达式。虽然性能影响很小,也通常不会影响到应用,但当变化通知多了之后损失也很客观。但是,这样做的好处是提供了编译时的类型安全,并且在重命名属性的时候支持重构。

Majirefy

Majirefy

喜欢折腾,喜欢各种各样的生活。曾经年少不懂事,看着别人写代码的样子感觉好帅,于是走上了半个不归路……然而,比起代码更喜欢写一些纯粹的文章,却经常因为自我不满意删掉重来。喜欢分享,无论是生活美好的瞬间,还是技术上的发现,虽然经常苦恼技术能力不强。由于喜欢买qiong买qiong买qiong,所以时常写一些类似使用体验的文章。

您可能还喜欢...

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注