分布式C/S开发系列:数据展示与用户体验一例(WPF)

C/S分布式开发相比BS开发要考虑更多问题,难度也相对要高。

本文以最基本的Client端请求展示数据为例来讨论一下C/S分布式开发中的用户体验!

 

在本文中你将看到关于C/S分布式设计中可能需要考虑的问题,MVVM模式的应用,Frame控件在WPF导航中无法适用需求的问题、Prism Region的应用等

 

首先,我们看下QQ当中对于用户资料查询时的UI设计:

1、加载过程

image

在这个过程当中,我们的数据是通过服务从服务端请求而来,请求过程存在一定的耗时操作,所以我们通常都会采用异步请求方式,确保UI正常!

2、请求失败的情况

image

对于这种远程服务请求来说,必然会遇到网络异常或请求失败的情况,或许很多人认为这种几率几乎不存在,或这认为您的应用是内联网应用不需要考虑;

我个人认为作为一个好的设计来说,要尽可能的考虑到所有情况,即便是理论上有可能不存在的情况。

 

好,上面2则演示我们看到了最简单的对于C/S方式客户端远程请求的一个处理示例;

接下来,我们再考虑一下分布式存在的情况,

image

如上图展示,我们注意当前窗口中有一个“更新”按钮, 在QQ客户端这个例子当中,我们对于资料查询这个页面来说不会是持久化展示的一个页面,

所以假设该页面中的数据在后台 或者 被其他客户端修改后 , 在当前客户端中并没有自动刷新,QQ的效果是您需要查看的时候手动去点击“更新”

 

而我们常见的BS系统与C/S系统很大的一个区别在于大多数BS系统的页面跳转频率较高,当每次页面跳转产生后会自动请求数据,所以不需要特别考虑数据一致性问题!(特殊应用系统除外

 

如果我们当前的应用是一个监测系统或实时信息显示系统,那么必然要求我们的页面处于持久展示状态,在这样的情况下页面就需要能够及时刷新数据;

在BS当中我们通常通过ajax来异步不断的刷新页面中需要及时展示的数据;那么C/S系统当中如何做呢? 也像ajax一样不断的去异步请求吗?

 

我个人认为死循环的异步请求并不合适,在移动应用中流量就是一个需要考虑的问题,还有如果数据没有变动,这样的异步请求是否耗费太多的资源!

 

 

好,我们就来实际考虑一下如何进行这样一个设计;

首先以一个稍微复杂Client端数据请求与查询为例来实现类似QQ的良好体验:

image 在这个描述图中,我们的导航区域 与 主内容区域的数据都是从服务端请求来的,类似QQ资料查看窗口(区别在于QQ的导航区域是固定的)

 

按照前面QQ所展示的,[导航区域]、[主内容区域] 都有可能请求失败,那就要求如果请求失败就要展示“错误页面”,且“错误页面”可以进行<刷新> 重新加载数据;

在上图的描述中我们的[主内容区域]是通过[导航区域]的“导航”后产生的结果,所以它的数据加载应该是由 [导航区域]的 SelectedChanged事件触发;

 

这样,我们就需要考虑这个“资料查看”窗口应该是分2部分区域,每部分区域展示属于它自己区域的页面,我们来看下在WPF中如何设计这个窗口;

<Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition MinWidth="150" MaxWidth="200"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Frame NavigationUIVisibility="Hidden" Source="/Views/NavigatePage.xaml"  Grid.Column="0"/>
            <Frame NavigationUIVisibility="Hidden" Source="/Views/MyPage.xaml"  x:Name="myContent" Grid.Column="1"/>
        </Grid>

通过放置两个Frame控件来承载不同的2个区域的内容页面;

继续,我们考虑下通过选中导航项如何通知[主内容区域] 进行内容加载;既然是导航,那必然涉及到导航参数,这时候我们要考虑如果在2个页面进行参数传递;

 

我们可以通过选择导航项来控制[主内容区域]的Frame进行Navigate,同时传递参数;

 

在这里我先通过MVVM模式中由ViewModel通讯来演示如何实现这一步;

 class NavigatePageViewModel:ViewModelBase<NavigatePageViewModel>
    {
        public NavigatePageViewModel()
        {
            m_NavigateCommand = new DelegateCommand<string>(this.NavigateCallback);
        }
 
        ICommand m_NavigateCommand = null;
        public ICommand NavigateCommand
        {
            get { return m_NavigateCommand; }
        }
 
        void NavigateCallback(string args)
        {
            this.SendMessage("Navigate", new NotificationEventArgs(args));
        }
    }
我们通过导航项绑定Command,再通过Command发送消息的方式来通知内容页面接收参数并加载数据 (注意:这里所说的内容页面 并非内容展示区域,通过MessageBus进行的消息通讯是VM之间的,并不能跨VM与View)
 
接着我们看下内容展示页面接收到导航参数之后做怎样的操作,
1、注册接收消息(MVVM模式中的应用)
 public MyPageViewModel()
        {
            this.RegisterToReceiveMessages("Navigate",
                new EventHandler<NotificationEventArgs>((sender, e) =>
                {
                    m_ReceiveMessage = e.Message;
                    this.AsyncLoad();
                }));
        }

 

我们来分析下段流程; 导航--->发送消息到内容展示页的VM中--->保存导航参数--->加载数据,有什么问题吗?

假设我们[主内容区域]当前展示为“错误页面”,此时我们的主内容页面即便接收到消息并且成功加载了数据,我们的View还是没有正确展示出来,(这里主要的问题是我们使用MVVM模式将逻辑与UI彻底隔离,仅进行数据驱动的结果)

 

既然我们发现这里的问题,那我们就需要修改下流程: ---->保存导航参数--->让主内容区域显示内容页面--->加载数据   ,这个时候就需要ViewModel通知View了,我们可以使用事件进行通知,

例如:

 EventHandler handler = this.Loaded;
                    if (handler != null)
                    {
                        handler(this, new EventArgs());
                    }

View页面中接收该事件并执行【让主内容区域展示内容页面】这一逻辑;

 private void MyPageViewModel_Loaded(object sender, EventArgs e)
        {
            var service = NavigationService.GetNavigationService(this);
            if (service != null)
            {
                service.Navigate(this);
            }
        }

 

如果我们使用了Frame作为主内容区域,到这里你就会发现无法获取NavigationService对象,因为当前Frame中存放的是“错误页面”

当然还有另外一个非常重要的问题:Frame在进行导航的时候 每次都会创建新的页面(类似与浏览器每次打开一个连接就会重新请求页面),这时候你就发现你缓存在ViewModel中的数据是没用的,这里你就无法解决上面提到的问题 “设我们[主内容区域]当前展示为“错误页面””

 

到这里我们就先暂停一下,先不来考虑UI上的问题,我们来讨论一下分布式下如何较好的处理客户端请求数据问题;

image   在这个图中,我描述了一个简单的发布-订阅 模式,也就是要求我们的客户端需要订阅服务端的一个服务,当服务端自己判断到需要推送数据给客户端的时候它主动进行推送一个消息,这个时候客户端会收到这个消息,然后客户端主动请求服务端刷新数据。(请注意:这里服务端不是主动推送数据给客户端,而是告诉客户端一个消息让它知道自己该刷新数据了)

 

在《WCF服务编程》一书中,作者已经附带了一个发布-订阅框架,我们可以直接拿来进行使用,经过测试基本能够满足这里我们所说的需求。

 

这里还有一点需要说明,客户端收到订阅的推送消息后 是主动请求刷新 还是被动请求刷新的问题, 在文章一开始我们看到的QQ的资料查看窗口中,它并没有订阅任何东西,仅仅是最简单的提供一个刷新按钮,

我们需要根据具体的应用场景来设计自己的需求,在QQ的例子中这应该是最好的方式,而在一个实时信息展示版中最好的应该是实时自动刷新,如果在一个即不需要实时刷新又需要持久展示的页面来说怎么办呢?

 

我认为最好的方式就是,服务端推送消息,客户端收到订阅的消息,同时在UI上提示有新的数据,让用户自己选择手动刷新(因为某些场景下我们不能替用户做主自动去刷新他正在浏览的页面),不知道您同意我的想法吗?

 

---------------------------------------------------------------------------------------------------------------------------------

 

好了,关于分布式如何更好的请求与展示数据这里就不多说了,这方面还是比较复杂的,目前我也希望能够学习到更多经验;

我们现在继续来解决前面UI展示的问题,Frame在这里已经不能满足我们的需求了,我们考虑用Prism的Region来操作;

 

如果你对Prism了解的话,应该知道prism框架是与IOC紧密相连的,涉及到IOC与DI 就会设计到生命周期问题,包括Region区域中的内容页面的生命周期问题;

在Prism框架的应用中,Region区域的内容都是通过 ServiceLocator服务定位器来查找并添加进去的,所以就要求我们的View都配置为 依赖输出项,以MEF为例就是需要View页面标注[Export]

 

对主窗口的改造很简单,无非是把Frame进行一下替换:

<DockPanel>
        <ContentControl  MinWidth="100" MaxWidth="200"
                        DockPanel.Dock="Left"
                        prism:RegionManager.RegionName="NavigationRegion" />
        <ContentControl prism:RegionManager.RegionName="ContentRegion"/>
    </DockPanel>

其他的部分也基本与之前提到的相似,比如:

 //接收导航参数
            this.RegisterToReceiveMessages("Navigate",
                new EventHandler<NotificationEventArgs>((sender, e) =>
                {
                    //缓存参数
                    m_ReceiveArgs = e.Message;
 
                    //通知View已接收到导航参数
                    EventHandler handler = this.Requested;
                    if (handler != null)
                    {
                        handler(this, new EventArgs());
                    }
                }));
 
这样的话,就需要在 内容页面 的View中接收对应的事件做不同的操作,类似如下:
/// <summary>
        /// 由ViewModel通知自身已接收导航参数
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ContentPageViewModel_Requested(object sender, EventArgs e)
        {
            //首先确保内容区域为当前View
            m_RegionManager.RequestNavigate("ContentRegion",
                "ContentView");
        }
 
        /// <summary>
        /// 由ViewModel通知异步数据加载失败
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ContentPageViewModel_Errored(object sender, EventArgs e)
        {
            StringBuilder bulider = new StringBuilder("ErrorView");
            var query = new UriQuery();
            query.Add("ErrorMessage", "模拟加载失败");
            bulider.Append(query);
            //使内容区域跳转至ErrorPage
            m_RegionManager.RequestNavigate("ContentRegion",
                bulider.ToString());
        }
 
 
OK,看到这些,我们不仅要问,那如何让我们的加载数据 这个逻辑去执行呢?
 
当我们使用Prism框架的时候我们知道有一个接口 INavigationAware,因为整个Prism的应用中是通过ServiceLocator来定位依赖注入的对象的,所以我们的View和ViewModel 都可以实现这个接口
在它的OnNavigatedTo方法中我们就可以进行数据加载了:
 
 public void OnNavigatedTo(NavigationContext navigationContext)
        {
            ContentViewModel viewModel = this.DataContext as ContentViewModel;
            if (viewModel != null)
            {
                //通知ViewModel异步加载数据
                viewModel.AsyncLoad();
            }
        }

 

相对来说,如果加载失败后跳转到了错误页面,那我们只需要让Region 进行返回导航就可以了,那通过Region怎么去导航和管理导航历史?有兴趣的可以去参看Prism框架应用!

实际的使用类似如下:

  IRegionNavigationJournal m_Journal = null;
        public void OnNavigatedTo(NavigationContext navigationContext)
        {
            //根据参数显示错误信息
            m_ErrorMessage = navigationContext.Parameters["ErrorMessage"];
            this.NotifyPropertyChanged(m => m.ErrorMessage);
 
            //保存Journal对象
            m_Journal = navigationContext.NavigationService.Journal;
        }
 
 private void BackCallback()
        {
            //注意此处:由于目前是模拟操作,ContentViewModel中已经缓存了 导航参数,
            //如果缓存的导航参数是Faild,那么这里使用Goback()方法无疑会使主内容区域循环展示为ErrorView
            //所以此处暂时使用SendMessage给ContentViewModel的方式来模拟使 “重新刷新”生效
            //this.SendMessage("Navigate", new NotificationEventArgs("Success"));
 
            
            if (m_Journal != null)
            {
                m_Journal.GoBack();
            }
        }

 

以上这部分基本都是关于Prism框架的简单的应用, 需要注意的就是在这里我们的页面与之前我们使用Frame导航有所区别,这里的页面生命周期都是由IOC去控制的,如果没有特别指明,一般都是单例的;

 

OK,我们还需要考虑一个问题,前面提到了View页面的生命周期问题,如果[导航区域]和[内容区域]的展示都出现错误,那都需要展示错误页面, 我们的错误页面通过导航历史去进行返回的时候怎么办呢?

其实这就是为什么我要提到IOC管理生命周期的原因,我们只需要让ErrorView 也就是错误页面的生命周期模式为 “NonShared” 就可以了,

 

例如:

 [Export]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public partial class ErrorView : UserControl
    {
        public ErrorView()
        {
            InitializeComponent();
        }
    }

 

 

有了以上的所有步骤,我们就可以模拟出类似QQ的资料查看窗口的一个用户体验效果了(对Loading\Loaded事件没有添加,有需要完全了解的可以看我的上篇文章中程序示例 点此传送)!!!

 

(文中所使用的UI技术以及各类框架都只是我们的一种工具,QQ并没有用WPF也没有Prism框架的Region,这里我只是演示一种形式,希望大家能理解我的意思)

 

最后,结合我们讨论的分布式情况下的客户端数据请求方式,加上这种分区域的利用页面来展示数据的方式,我想大家一定能够设计出比较不错的用户体验;

 

这里将窗口区域的演示项目上传,希望能帮到有需要的朋友进行学习,  同时非常欢迎与大家讨论C/S模式的分布式开发,很希望能向大家请教经验!

/Files/cxwx/MyTest.zip 

示例项目中需要的第三方框架都是nuget来的,为了保证附件大小没有打包组建,需要组件的自己nuget就可以;

posted @ 2012-01-12 12:58  lianghugg  阅读(3283)  评论(2编辑  收藏  举报