页面与ViewModel(上)

在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; }//页面的Dispatcher

        private 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旺信这种依赖于网络的应用的页面的实现中需要注意的一些地方。

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

时间: 2024-08-27 10:40:02

页面与ViewModel(上)的相关文章

基于jQuery的ajax系列之用FormData实现页面无刷新上传

接着上一篇ajax系列之用jQuery的ajax方法向服务器发出get和post请求写,这篇主要写如何利用ajax和FormData实现页面无刷新的文件上传效果,主要用到了jQuery的ajax()方法和XMLHttpRequest Level 2的FormData接口.关于FormData,大家可以看MDN文档. 1,先看效果图 期望的功能和效果很简单:点击页面中的上传文件表单控件,选择文件后点击"ajax提交",将文件上传至服务器,上传成功后,页面给出一个简单的提示. 2,前端的代

文件上传的动作不能太俗,必须页面无刷新上传

人生永远没有太晚的开始 好久没有更新博客了,说实话,每天抽空打开博客园总感觉心里很空虚,不是没有在修行,而是因为最近在跟着博老前辈完成一项很重要的使命——打造属于我们自己的奇遇帝国,好了废话不多说,首先我在北京向各位带好,希望各位能在2018年达到自己预设的人生高度. 常规操作上传文件 1 <form action="xxx.action" method="post" enctype="multipart/form-data"> 2

程序员在页面友好性上常犯的5种错误以及改正方法

我是一个性情乖戾的Web用户,但我想这也帮助促使我成为了一名优秀的Web开发人员.当我看到一个网站上有让人不爽的设计时就会非常的恼怒,一些很简单的东西为什么做不好?下面是5种常见的可用性方面的错误,以及如何纠正这些问题的方法.给自己方便,也与人方便,确保自己不要犯这样的错误. 使用表达submit事件,不要用click事件:请用表单标签form! 我不知道遇到过多少次,当我使用回车键提交一个表单时(或手机上用箭头/输入键),却什么都没发生.我只好又用鼠标点击提交按钮,表单终于有了反应.这是我最痛

网站页面识别基础上

今天学到了网页的基础知识.有:一.静态网页和动态网页的区分.静态网页是不连接数据库,后期如果想要更改网页的主页图片或产品需要程序员用源代码进行更改.而动态网页是非程序员也可以通过后台页面,增删图片的.通常静态网页作用多于静态网页.二.通过DW软件查看网页的代码,里面的代码含义以及一些IT的英文注解意思.1.在html中静态页面工具例如:①CSS:具有美化网页的作用:②java script:是脚本语言的意思.做特效用的.例如:网页的大图进行更换时,或建设二级或三级连接?③Xml:和Html的语法

用jQuery File Upload做的上传控件demo,支持同页面多个上传按钮

需求 有这么一个需求,一个form有多个文件要上传,但又不是传统的图片批量上传那种,是类似下图这种需求,一开始是用的swfupload做的上传,但是问题是如果有多个按钮的话,就要写很多重复的代码,于为了代码的简洁所以就开始寻求其他的方法,期间试过uploadify,但是由于样式始终调不出来,最后就放弃了,直到发现这么个小巧的玩意,jQuery File Upload. 本文包含了upload的js实现,html的分析,css的实现等内容,文章末尾有git地址 最简运行时 官网下载的demo有N多

h5-webAPP页面返回上个页面同时刷新上个页面

很多时候在创建一个订单时需要添加备注,而添加备注这个功能在UI设计时往往是,点击跳转到另一个页面(B)去输入,返回的时候给带到创建订单的页面(A).在APP中我们可以通过以下这种方式, A页面中: // 局部刷新 window.addEventListener('personCenter', function(e) { getUserInfo() }) B页面中: // 个人中心 var personCenter = plus.webview.getWebviewById('页面的ID'); /

在Vue项目中使用html2canvas生成页面截图并上传

引入cnpm install html2canvas. html代码 <!-- 把需要生成截图的元素放在一个元素容器里,设置一个ref --> <div class="image_tofile" ref="imageTofile"> <!-- 这里放需要截图的元素,自定义组件元素也可以 --> </div> <!-- 如果需要展示截取的图片,给一个img标签 --> <img :src="h

h5页面在iOS上的问题解决

1.ios移动端 软键盘收起后,页面内容被顶上去,不下滑回原处 代码如下: $(function(){ $('input,textarea').on('blur',function(){ window.scroll(0,0); }); $('select').on('change',function(){ window.scroll(0,0); }); }) 原理就是弹起键盘的时候,window.scrollY会从0变到键盘的高度(例如:200),当输入框焦点失去后让scrollY回到0就好了.

html5移动页面上传图片,上传文件的控制

function readURL(input) { if (input.files && input.files[0]) { var reader = new FileReader(); reader.onload = function (e) { //$('#uploadInput').removeAttr('src'); //$('#uploadInput').attr('src', e.target.result); $.ajax({ data:{file:e.target.resu