页面与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旺信这种依赖于网络的应用的页面的实现中需要注意的一些地方。
在下一篇博客中,笔者将进一步分享对于页面细节实现的体会。欢迎大家关注,拍砖,共同进步。
页面与ViewModel(上)的更多相关文章
- 页面与ViewModel(下)
在上一篇博客中,笔者分享了一些从页面整体的角度对页面与ViewModel的思考.在本文中笔者希望从相对细节的角度分享一些对页面与ViewModel的思考. 比如,当我们在更新View Model中的绑 ...
- IQKeyboardManager在某个页面取消键盘上面的Toolbar
使用IQKeyboardManager的时候,如果想在某个页面取消键盘上面的Toolbar,请使用如下方法,按控制器去操作 // 取消IQKeyboardManager Toolbar [[IQKey ...
- mvvm框架下页面与ViewModel的各种参数传递方式
传单个参数的话在xaml用 Command={Binding ViewModel的事件处理名称} CommandParameter={Binding 要传递的控件名称} ViewMode ...
- 基于jQuery的ajax系列之用FormData实现页面无刷新上传
接着上一篇ajax系列之用jQuery的ajax方法向服务器发出get和post请求写,这篇主要写如何利用ajax和FormData实现页面无刷新的文件上传效果,主要用到了jQuery的ajax()方 ...
- angular封装七牛云图片上传,解决同一页面多个上传按钮分别上传
step1:引入文件 引入Plupload *该SDK上传功能集于Plupload插件封装,所以需要下载Plupload; plupload.dev.js 引入qiniu.js为了简便,当时直接从官网 ...
- Django实现注册页面_头像上传
Django实现注册页面_头像上传 Django实现注册页面_头像上传 1.urls.py 配置路由 from django.conf.urls import url from django.cont ...
- webuploader的一个页面多个上传按钮实例
借鉴一位大佬的demo 附上他的github地址https://github.com/lishuqi 我把他的cxuploader.js改了不需要预览 直接上传图片后拿到回传地址给img标签显示图 ...
- 用javascript实现禁止页面后退返回上一页的代码
用javascript实现禁止页面后退返回上一页的代码: 有时候我们需要用户在点击了如下一步的按钮时,页面跳转到了下一个页面,这时想不允许用户返回后退到上一页,可以采用下面的方法: 在需要跳转的页 ...
- 用jQuery File Upload做的上传控件demo,支持同页面多个上传按钮
需求 有这么一个需求,一个form有多个文件要上传,但又不是传统的图片批量上传那种,是类似下图这种需求,一开始是用的swfupload做的上传,但是问题是如果有多个按钮的话,就要写很多重复的代码,于为 ...
随机推荐
- Java多线程基础学习(一)
1. 创建线程 1.1 通过构造函数:public Thread(Runnable target, String name){} 或:public Thread(Runnable target ...
- .NET 串口通信
这段时间做了一个和硬件设备通信的小项目,涉及到扫描头.输送线.称重机.贴标机等硬件.和各设备之间通信使用的是串口或网络(Socket)的方式.扫描头和贴标机使用的网络通信,输送线和称重机使用的是串口通 ...
- UWP中新加的数据绑定方式x:Bind分析总结
UWP中新加的数据绑定方式x:Bind分析总结 0x00 UWP中的x:Bind 由之前有过WPF开发经验,所以在学习UWP的时候直接省略了XAML.数据绑定等几个看着十分眼熟的主题.学习过程中倒是也 ...
- 算法笔记_013:汉诺塔问题(Java递归法和非递归法)
目录 1 问题描述 2 解决方案 2.1 递归法 2.2 非递归法 1 问题描述 Simulate the movement of the Towers of Hanoi Puzzle; Bonus ...
- InnoDB关键特性学习笔记
插入缓存 Insert Buffer Insert Buffer是InnoDB存储引擎关键特性中最令人激动与兴奋的一个功能.不过这个名字可能会让人认为插入缓冲是缓冲池中的一个组成部分.其实不然,Inn ...
- 一起学微软Power BI系列-使用技巧(1)连接Oracle与Mysql数据库
说起Oracle数据库,以前没用过Oracle不知道,但是这1年用Oracle后,发现真的是想狂吐槽,特别是那个.NET驱动和链接字符串,特别奇葩.总归是和其他数据库不一样,标新立异,不知道为何.另外 ...
- UWP开发之Template10实践二:拍照功能你合理使用了吗?(TempState临时目录问题)
最近在忙Asp.Net MVC开发一直没空更新UWP这块,不过有时间的话还是需要将自己的经验和大家分享下,以求共同进步. 在上章[UWP开发之Template10实践:本地文件与照相机文件操作的MVV ...
- 玩转spring boot——结合JPA事务
接着上篇 一.准备工作 修改pom.xml文件 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi=&q ...
- 推荐一个ASP.NET网站内容管理系统源码
许多人都有各自的兴趣,如打球.踢毽子.看书.看电视.玩游戏等等....我近来迷上了猜灯谜,于是业余做了一个在线猜灯谜的网站:何问起谜语. 先出个谜语让你猜猜:不可缺一点(打一字).可以在线猜:http ...
- ObserverPattern(观察者模式)
import java.util.ArrayList; import java.util.List; /** * 观察者模式 * @author TMAC-J * 牵一发而动全身来形容观察者模式在合适 ...