页面与ViewModel(上)

news/2024/7/7 21:42:25

在UWP淘宝与旺信中,笔者主要负责页面与控件的制作,这些工作看似简单,但要想做的全面细致仍然需要深入的思考。本文想分享一些在UWP旺信的制作过程中,笔者在UI页面与控件制作上体会到的一些心得。可能笔者的有些方法并不见得高明,或者仍需要时间的检验,所以也欢迎大家拍砖,共同进步。

UWP旺信是一个非常依赖网络的应用,在应用页面中的很多数据都需要访问网络才能取到最新的结果,这样一来网络状况就会影响到用户体验。为了把网络对用户体验的影响降低,在UWP旺信中采用了比较通用的做法:数据缓存。在进入页面以后先把缓存中的数据呈现给用户,然后在后台进行联网拉取最新的数据并更新页面显示。以UWP旺信中的群信息页面为例:在Loaded方法中,页面会先从缓存中获得群的数据并更新页面显示。接着页面继续调用网络接口,并通过接口返回数据对页面UI进行更新。

看起来是非常简单的过程,但是其中存在几个需要注意的问题:

首先,我们在UI页面上显示内容时,一般会采取绑定到后台数据的方法。而UI页面实际上还有很多状态,如各个元素的显示隐藏,Progress控件的激活与停止等等。这些状态往往也需要绑定到后台数据。如果我们把这些内容和状态的数据都放在页面的code Behind中,则会大大增加code Behind的复杂度,因此我们可以将这些内容和状态数据集中放在一个View Model类中,让UI页面的元素来绑定。

其次,一般数据绑定的方法有Binding 或 x:Bind。使用Binding方法会比较简单,只要在code Behind中设置页面的DataContext为View Model就可以绑定了。而由于微软官方已经明确了x:Bind方法在运行效率上是优于Binding方法的,那么我们应当优先使用x:Bind方法。但是x:Bind方法有个缺点就是它绑定的属性是页面或控件自身code Behind的属性,而不能灵活的选择不同类型的DataContext来进行绑定。因此我们可以将View Model作为Code behind的一个成员变量,这样一来也能实现页面对View Model中数据的绑定。

第三,在UWP应用中,当导航到一个页面时可以用OnNavigatedTo方法的参数来传递数据到该页面。但当从一个页面goback到之前的页面时,却没有方法来返回一个数据到之前的页面。对于这种情况有很多解决办法:可以使用全局变量,可以让前一个页面的缓存模式设为enabled或required并在导航到下一个页面时传递引用型变量参数等等。笔者在旺信中则尝试了把ViewModel做成单例,让相关页面使用同一个ViewModel的方法。例如旺信中群成员,群成员管理,添加群成员,群设置管理等页面是一系列相关的页面,需要统一从群成员页面进入,在应用中不存在同类型页面有多个实例同时存在的情况,并且有很多数据是共享的。于是笔者为它们创建了一个共同的ViewModel,在这些页面之间导航时,都使用一个ViewModel实例。这样就解决了数据回传的问题。并且用户操作后,各个页面都能同步更新。

第四,在更新View Model中与页面UI绑定的数据时,如果是从页面的code Behind中采用await的异步方法来更新的话,会非常顺利。UWP旺信获取群信息的接口是通过回调来返回数据的,当回调方法试图更新View Model中绑定到UI界面的数据时,会触发异常,提示"The application called an interface that was marshalled for a different thread."。这是由于回调方法一般来说并不是由UI线程发起的,而页面绑定的数据只能通过UI线程来修改。如果一定要通过其他线程来修改,需要使用页面的Dispatcher的RunAsync方法来进行。因此在View Model中我们需要增加一个CoreDispatcher成员变量,在页面初始化View Model时,将页面的Dispatcher赋值给该变量。当回调方法更新View Model时,如果CoreDispatcher成员变量不为空,就调用CoreDispatcher来更新绑定数据。

根据这些注意点,以旺信的群成员页面为例:

在xaml页面中群成员列表数据源绑定了View Model中的mainList列表变量:

    <Page.Resources><CollectionViewSource x:Name="ContactsCVS" Source="{x:Bind thisData.MainList,Mode=OneWay}"  IsSourceGrouped="True" /></Page.Resources>

 而在code Behind中,声明了thisData变量作为ViewModel:

public sealed partial class TribeCardMorePage : BasePage{//private TribeCardMoreVM thisData;//}

 并且在页面初始化时把ViewModel类型的单例赋值给它,并将页面的Dispatcher传递给ViewModel:

        public TribeCardMorePage(){this.InitializeComponent();thisData = TribeCardMoreVM.Instance;thisData.dispatcher = Dispatcher;}

 在页面的OnNavigatedTo方法中设置ViewModel的一些参数,并尝试从缓存中取出缓存的数据:

        public override async void OnNavigatedTo(NavigationEventArgs args){base.OnNavigatedTo(args);if (args != null && args.Parameter != null && args.Parameter is Tribe){thisData.para = args.Parameter as Tribe;}thisData.LoadDataFromCache();//
}

在页面的Loaded事件中再通过网络更新数据:

        protected async override void OnLoaded(RoutedEventArgs e){base.OnLoaded(e);await thisData.LoadData();}

而ViewModel则是一个继承了INotifyPropertyChanged接口的数据类型,这样ViewModel中数据的变化才能通知到页面,数据绑定才有效。一般我们会写一个类来继承INotifyPropertyChanged接口,而ViewModel则继承这个类就可以了。

    public class ObservableObject : INotifyPropertyChanged{public event PropertyChangedEventHandler PropertyChanged;protected void RaisePropertyChanged([CallerMemberName]string propertyName = null){var handler = PropertyChanged;if (handler != null)handler(this, new PropertyChangedEventArgs(propertyName));}}

如上所述,ViewModel需要继承ObservableObject类,要有供UI绑定的数据,要有更新数据用的Dispatcher,数据取回之后要用Dispatcher来更新:

    public class TribeCardMoreVM : ObservableObject{//private ContactMgr _cmgr = new ContactMgr();private volatile static TribeCardMoreVM _instance = null;public static TribeCardMoreVM Instance //ViewModel的单例
        {get{if (_instance == null){_instance = new TribeCardMoreVM();}return _instance;}}public CoreDispatcher dispatcher { get; set; }//页面的Dispatcherprivate Tribe _para;public Tribe para //获取数据的参数
        {get { return _para; }set{_para = value;RaisePropertyChanged();}}private ObservableCollection<TribeMemberUIGroup> _MainList = new ObservableCollection<TribeMemberUIGroup>();public ObservableCollection<TribeMemberUIGroup> MainList//页面绑定的数据
        {get { return _MainList; }set { _MainList = value; RaisePropertyChanged(); }}public async Task LoadData(){//_cmgr.OnOnlineContactComplete += (ex, tx) =>{dispatcher?.RunAsync(CoreDispatcherPriority.Normal, () =>  //在回调方法中用dispatcher来更新页面UI
                {updateTribeMemberUIGroup();});};await _cmgr.OnEventOnlineContactList(); //从网络获取数据//
        }//
}

以上这些就是笔者所体会到的在类似于UWP旺信这种依赖于网络的应用的页面的实现中需要注意的一些地方。

在下一篇博客中,笔者将进一步分享对于页面细节实现的体会。欢迎大家关注,拍砖,共同进步。


http://lihuaxi.xjx100.cn/news/244310.html

相关文章

IDEA2020如何设置全局maven路径

个人免费资源分享网站&#xff1a;http://xiaocaoshare.com/ File —> New Prijects Settings —> Setting for New Projects…

预示敏捷方法走偏的15个标志——第1部分

2019独角兽企业重金招聘Python工程师标准>>> 【编者按】误解和“最佳实践”可能会让你的团队原地打转&#xff0c;无法高效产出代码。本文主要介绍预示着敏捷方法走偏的15个标志&#xff0c;作者为 Steven A. Lowe。文章系国内 ITOM 管理平台 OneAPM 编译呈现。 要赶…

nginx的安装和使用(一)

个人免费资源分享网站&#xff1a;http://xiaocaoshare.com/ 1.下载安装nginx,启动 点击的时候可能一闪而过&#xff0c;这个不影响&#xff0c;直接在浏览器上输入locahost,出现以下页面说明安装成功 如果要把项目部署到nginx&#xff0c;先找到conf下的nginx.conf文件 因为修…

plsql 导入导出表、数据、序列、视图

&#xfeff;&#xfeff;一、导出&#xff1a; 1、打开plsql-->工具----》导出用户对象(可以导出表结构和序列、视图) ps:如果上面不选中"包括所有者",这样到导出的表结构等就不包含所有者&#xff0c; 这样就可以将A所有者的表结构等导入到B所有者的表空间中 2、…

41.国际化

转自&#xff1a;https://wenku.baidu.com/view/84fa86ae360cba1aa911da02.html 尽管国际化不是重点内容&#xff0c;但是也有必要了解它的使用。在struts2中国际化有三种级别&#xff1a;分别是针对某个Action的action级别&#xff0c;针对package的package级别&#xff0c;针…

IDEA如何导入多个maven依赖的项目

个人网站&#xff1a;http://xiaocaoshare.com/ 1.file->open找到文件的路径导入 导入进来的只是以文件夹形式&#xff0c;需要在project Structure再次导入 设置项目jdk 导入依赖的maven项目&#xff0c;先导入父工程 导入完之后对应的工程发生改变 同理&#xff0c;依次…

Js----闭包

1、闭包的概念&#xff1a;(我找了很多&#xff0c;看大家的理解) A:闭包是指可以包含自由&#xff08;未绑定到特定对象&#xff09;变量的代码块&#xff1b; 这些变量不是在这个代码块内或者任何全局上下文中定义的&#xff0c;而是在定义代码块的环境中定义&#xff08;局部…

iOS原生如何加载HTML中img标签的图片

原文出自&#xff1a;iOS原生如何加载HTML中img标签的图片 前言 最近iOS App项目中使用Webview加载H5页面比较多&#xff0c;也有不少朋友经常问到这个问题&#xff0c;在这里我也学习学习如何通过iOS原生的方式来加载H5页面中的图片然后让webview显示图片。 相信有很多朋友也…