前言

之前在 剁手党也有春天 -- 淘宝 UWP ”比较“功能诞生记 这篇随笔中介绍了一下 UWP 淘宝的“比较”新功能呱呱坠地的过程。在鲜活的文字背后,其实都是程序员不眠不休的血泪史(有血有泪有史)……所以我们这次就要在看似好玩的 UWP 多窗口实现背后,挖掘一些我们也是首次接触的干活“新鲜热辣”地放松给大家。希望能使大家在想要将自己的 APP 开新窗口的时候,能从本文中得到一些启发,而不是总是发现 C# 关于 UWP 开新窗口可供参考的文章只有 Is it possible to open a new window in UWP apps?这一篇。

---------我是干(一声)活(四声)的分割线--------

多开窗口的实现

由于主窗口各功能趋于稳定,而且很难腾出一块较大的空间给比较功能,而且如果需要再额外划分出一块空间的话,势必会增加用户来回切换空间的操作,从时间成本和学习成本来说都是不够高效的,所以我们决定利用一下 UWP 的新的功能,新打开一个窗口,这样可以在新窗口中完整体验比较功能。

所以本文最主要的目的,当然就是借我们的新的比较功能,谈一谈 UWP 新窗口功能的实现,以及窗口直接信息的传递和互动。要实现多窗口操作,首先“你得有一个女朋友”……不对,是你得有一个新窗口。那么如何打开新窗口呢?

UWP 开启新窗口
UWP 新开启第二窗口的步骤不算难,

                CoreApplicationView newView = CoreApplication.CreateNewView();
await newView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
var newWindow = Window.Current;
var newAppView = ApplicationView.GetForCurrentView();
frame = new Frame();
frame.Navigate(typeof(ComparisonPage));
newWindow.Content = frame;
newWindow.Activate();
newViewId = newAppView.Id;
});
viewShown = await ApplicationViewSwitcher.TryShowAsStandaloneAsync(newViewId);

简单几句就可以打开一个新窗口,并且在新窗口中切换到事先写好的“比较页面”。但是这样打开的新窗口还比较“粗糙”,很大的几率会出问题,例如打开了更多的窗口。那么需要我们一步一步完善:

1. 样式问题:

新窗口中,窗口的标题栏是 Windows 当前主题的颜色,和主窗口的淘宝主题色很不搭调。怎么办?

加入这么几行代码:

                    newAppView.Title = "商品比较";
ApplicationViewTitleBar titleBar = newAppView.TitleBar;
titleBar.BackgroundColor = ......;
titleBar.ForegroundColor = ......;

其中,titleBar 的参数是可以充分进行设定的。这样我们就可以实现和主窗口一样的色调,使新窗口看起来不那么“山寨”。

2. 用户回到主界面,再点击一次“去比较”按钮,又会新开好多窗口,这个怎么办呢?

这个问题其实不难解决,我们注意到,最后打开新窗口的 TryShowAsStandaloneAsync 方法会根据是否打开成功返回一个 bool 值,我们可以根据这个 bool 值进行判断,如果为 true,说明新窗口已经打开了,那我们只需要执行

                    await ApplicationViewSwitcher.TryShowAsStandaloneAsync(newViewId);

就可以切换到刚才的窗口了。

3. 要是打开的比较窗口被用户关闭了怎么办呢?

的确,要是打开新窗口成功,然后关闭的话,仅仅判断 TryShowAsStandaloneAsync 方法的返回值是不够的,很有可能出现跳转到一个不存在的窗口 id 的情况。所以我们再引入一个 bool 值,叫viewClosed,当 viewClosed 为 true 的时候,说明用户关闭了新的比较窗口,那么再次点击“去比较”的时候,我们就不能单纯跳转,而是要再次打开刚才的窗口。首次打开新窗口的时候,为新窗口的 Consolidated 事件触发方法,这样就可以在用户关闭新窗口的时候,将 ViewClosed 置为 true。这样,我们就可以根据 viewClosed 和 viewShown 来判断当前窗口的情况。从而做出正确的选择了。

                    newAppView.Consolidated += NewAppView_Consolidated;
...... } private void NewAppView_Consolidated(ApplicationView sender, ApplicationViewConsolidatedEventArgs args)
{
viewClosed = true;
}

这样,整体打开新窗口的较完整代码结构就变成了:

        static bool viewShown = false;
static bool viewClosed = false;
static int newViewId;
static int currentViewId;
static Frame frame; private async void AppBarFontButton_ComparisonButtonTapped(object sender, bool e)
{
CoreApplicationView newView = CoreApplication.CreateNewView();
if (viewShown)
{
if (viewClosed)
{
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(newViewId);
viewClosed = false;
}
else
{
await ApplicationViewSwitcher.SwitchAsync(newViewId);
}
}
else
{
await newView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
var newWindow = Window.Current;
var newAppView = ApplicationView.GetForCurrentView();
newAppView.Consolidated += NewAppView_Consolidated;
newAppView.Title = "商品比较";
ApplicationViewTitleBar titleBar = newAppView.TitleBar; // Title bar setting
...... frame = new Frame();
frame.Navigate(typeof(ComparisonPage));
newWindow.Content = frame;
newWindow.Activate();
newViewId = newAppView.Id; });
viewShown = await ApplicationViewSwitcher.TryShowAsStandaloneAsync(newViewId);
}
} private void NewAppView_Consolidated(ApplicationView sender, ApplicationViewConsolidatedEventArgs args)
{
viewClosed = true;
}

这样,就基本可以做到在主窗口不管怎样点击,或者新窗口不管是不是关闭了,都可以一键切换到我们的比较窗口了。下一步,我们的目标就是要将当前的商品传递到比较窗口进行展示。

参数与事件的互相传递

主窗口向子窗口传递参数:

由于主窗口是商品详情页面,所以当前页面已经拥有了导航到此商品的全部导航信息。但是如何可以将这些信息传递到子窗口呢?我们注意到,刚才子窗口的页面的导航方法是:

                    frame = new Frame();
frame.Navigate(typeof(ComparisonPage));
newWindow.Content = frame;

这种导航方式,使得我们很难访问被导航页面的信息,从而难以传递信息。那是不是就没有办法了么?当然不是,这里提供两种思路,供不同场景下参考:

方法1:静态参数

将 ComparisonPage 页面的商品导航参数对象设置为静态,这样就可以通过

ComparisonPage._navArgs = _navArgs;

的方法,在主页面直接赋值。然后可以通过触发其他静态方法或者为这个导航参数对象继承 INotifyPropertyChanged 接口,这样当被赋值的时候可以触发事件,使得新窗口在比较栏中打开这个新的商品。由于每次只有一个主窗口,也只有一个页面可以点击去比较,所以不太可能出现多个页面同时向一个静态参数传递信息导致冲突的情况发生。

方法2:强行找到这个被导航到的页面的对象并赋值

这个方法说起来有点拗口,但其实就是找到 frame 实际导航到的页面,并对其对象(非静态)进行赋值。这样,我们需要用到一个方法叫做 FindVisualChildren,其实现如下:

        public static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T)
{
yield return (T)child;
} foreach (T childOfChild in FindVisualChildren<T>(child))
{
yield return childOfChild;
}
}
}
}

通过这个方法,我们可以用

            foreach (ComparisonPage cp in FindVisualChildren<ComparisonPage>(frame))
{
cp._navArgs = _navArgs;
}

来找到这个页面的参数。我们还可以用这个方法来调用这个页面的非静态方法,这样也就可以很方便地触发页面下的商品跳转功能了。

子窗口与主窗口交互:

子窗口有两个机会,十分有幸地向上和主窗口进行交互:

一是在商品未填满所有比较窗口的时候,我们可以一键返回主窗口,继续挑选商品加入比较。

二是点击待比较商品的店铺,会在主窗口跳转到店铺。

1. 子窗口切换到主窗口

这个问题相对简单,其实在子窗口就是一句代码的事:

        private async void SwitchToMasterWindow(object sender, int e)
{
await ApplicationViewSwitcher.SwitchAsync(masterWindowId);
}

但是问题在于,子窗口怎么知道主窗口的 masterWindowId 呢?所以,还是要靠主窗口在创建子窗口的时候,把自己的 id 无私地告诉子窗口:

            var currentView = ApplicationView.GetForCurrentView();
currentViewId = currentView.Id;
...
frame.Navigate(typeof(ComparisonPage), currentViewId);

这样子窗口就可以一键回家吃饭了!

2. 子窗口通知主窗口跳转店铺

这个问题就比单纯窗口切换要难一些了。在试过多次子窗口跳转主窗口然后跳转店铺被报线程错误但是解决无果后,我只能祭出笨却实用的老办法:事件通知。子窗口点击店铺的时候,触发跳转店铺事件,同时参数是店铺的 id,主页面创建子页面的时候,注册这个事件,一旦触发,就捕捉事件参数(店铺 id)进行跳转。至于注册这个事件,既可以用刚才提过的静态参数法,也可以用 FindVisualChildren 这个好用的方法,直接把事件从页面里抓出来进行注册:

        private void Frame_LayoutUpdated(object sender, object e)
{
foreach (ComparisonPage cp in FindVisualChildren<ComparisonPage>(frame))
{
cp.GoToShop -= Cp_GoToShop;
cp.GoToShop += Cp_GoToShop;
}
} private async void Cp_GoToShop(object sender, string e)
{
await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
if (!string.IsNullOrEmpty(e))
{
Nav.To(DataHelper.DataSource.ShopDS.GetH5ShopIndexUrlByShopId(e));
}
});
}

3. 未登录状态打开比较窗口遇到的问题

这是一个在写作过程中被报的 Bug, 如果在未登录状态下打开比较页面,那么在点击“登陆”和“加入购物车”的时候程序会崩溃。“哦!我的天哪!我的老伙计,这确实是我的问题。非常感谢你们能把它提出来。”(央视翻译腔)。由于我在写代码和测试过程中,一直是有账号登陆的状态,所以确实忽略了未登录状态可能遇到的问题。那么为什么会出现这个问题呢?是因为默认的页面设计是:如果遇到“收藏”或“购物车”这些需要登录才能进行的操作时,会调用另外的登陆控件填充屏幕,使用户登录。而在新窗口中,受到线程的制约(具体情况下文会讲到),在调用另外的控件会出现线程间调用的错误。而这些“收藏”或“加入购物车”都是控件级别的事件,难以用页面级别的 UI 线程处理这个问题;同时为了避免在三个比较窗口都弹出登陆提示框(用户到底登陆哪个算?),我们决定将登陆事件向上传,传到比较页面的顶层,然后提示用户是否要登陆?如果登陆,则切换回主窗口进行登陆,否则则暂不登陆。

所以这里的处理方法和刚刚提到的子窗口通知主窗口跳转店铺很相似,提示跳转 -> 跳转 -> 传递事件:

        private async void Tdp_UserNotLogin(object sender, string e)
{
bool ret = await ShowDialog(string.Format("亲,你还没有登陆,是否要切换到主窗口登陆?"), "去登陆", "先不登陆");
if (ret)
{
await ApplicationViewSwitcher.SwitchAsync(masterWindowId);
UserWantstoLogin?.Invoke(this, e);
}
}

然后由主页面处理登陆事件,这样可以避免同时打开多个登陆窗口造成混乱的情况。

4. 子窗口随主窗口关闭

这也是一个在写作过程中被报的 Bug。那就是,关闭了主窗口,子窗口不会随之关闭,导致整个进程不结束,只有关闭了子窗口才算是全部关闭完成。这个问题其实不难解决,我们首先获得主窗口的“View”,然后在这个“View”的 Consolidated 事件上加入关闭程序的指令(静态方法)即可:

            var currentView = ApplicationView.GetForCurrentView();
currentView.Consolidated += CurrentView_Consolidated;
...... private void CurrentView_Consolidated(ApplicationView sender, ApplicationViewConsolidatedEventArgs args)
{
CoreApplication.Exit();
}

当然如果你是一个怀旧的人,也可以使用较为老派的(非静态方法)

Application.Current.Exit();

参考:

How to exit or close an UWP app programmatically? (Windows 10)

和线程作斗争,一头乱麻

相信大家都听过这个关于多线程的著名笑话:“从前我有一个问题,后来我用多线程去解决这个问题,现在我有了两问个题”。

这个笑话告诉我们多线程最容易带来混乱,尤其是 UWP 这些数不清的异步方法,稍微一不注意就会抛出异常。很多细心的读者应该注意到了,我在之前的很多地方的代码都用到了:

                await newView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
......
});

               await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
......
});

这其实就是在通知 UI 线程进行异步操作(这里用 Lambda 表达式代替了过时的代理方法),在新窗口和老窗口的一些交互的地方,例如老窗口创建新窗口,新窗口展示待比较宝贝页面,如果不使用线程代理的话,都是会提示出错导致 App 崩溃的,所以都需要用这个方法来通知 UI 线程进行异步操作。如果要写成同步的,代码就要麻烦许多。或者还有刚刚提到的新窗口未登录状态需要打开登陆页面的情况,涉及到线程过于复杂,所以干脆就用事件传递到主窗口进行处理。如果详细展开说的话,仅仅这一段就可以再写好几篇博客了。所以我们在这里不再讨论过于底层的东西,因为这些和 WPF 都是技术相通的,很多人都写过关于这个的文章,因此我们不再赘述。如果读者感兴趣的话,不妨读一下关于 UWP 或 WPF 线程的文章,获取更深层的知识。如果可以达到这个目的,那么也算是我们抛砖引玉了。

总结

UWP 开新窗口不难,但是要想很好的让新窗口和主窗口老老实实为你工作,就需要花一点心思和不断地调教他们了(其实都是程序员的自我调教)。我们不但要注意各个窗口的状态,知道在什么时候使用跳转什么时候使用打开窗口,还需要通过各种办法在窗口之间传递信息和事件。但即使我们每一点都测试到了,还是容易受到多线程的拖累或者产生一些意想不到的问题。我只能说,和多窗口打交道的日子,绝对是痛并快乐着。

参考:

[UWP]Is it possible to open a new window in UWP apps?

Find all controls in WPF Window by type

How to exit or close an UWP app programmatically? (Windows 10)

从淘宝 UWP 的新功能 -- 比较页面来谈谈 UWP 的窗口多开功能的更多相关文章

  1. 淘宝上倒卖新浪微盘事件来龙去脉——谈谈巧用IMEI

    这是一个老黄历的事件,曾记得淘宝上的卖家卖10元卖50g网络硬盘,并且卖的相当的火,一个月就卖了500个账号.由于我也是那个事件的亲身经历者之一,这里就看到了IMEI号在项目中防止作弊是何其的重要. ...

  2. 模仿淘宝首页写的高仿页面,脚本全用的原生JS,菜鸟一枚高手看了勿喷哈

    自己仿照淘宝首页写的页面,仿真度自己感觉可以.JS脚本全是用原生JavaScript写得,没用框架.高手看了勿喷,请多多指正哈!先上网页截图看看效果,然后上源码: 上源码,先JavaScript : ...

  3. 爬取淘宝商品信息,放到html页面展示

    爬取淘宝商品信息 import pymysql import requests import re def getHTMLText(url): kv = {'cookie':'thw=cn; hng= ...

  4. 剁手党也有春天 -- 淘宝 UWP ”比较“功能诞生记

    前言 网购已经不再是现在的时髦,而变成了我们每天的日常生活.上网已经和买买买紧密地联系在了一起,成为了我们的人生信条.而逛街一词,越来越多地变成了一种情怀.有时候我们去逛街,要么是为了打发时间,要么是 ...

  5. “四核”驱动的“三维”导航 -- 淘宝新UI(需求分析篇)

    前言 孔子说:"软件是对客观世界的抽象". 首先声明,这里的"三维导航"和地图没一毛钱关系,"四核驱动"和硬件也没关系,而是为了复杂的应用而 ...

  6. 读《淘宝技术这十年》 总结下web架构的发展

    关键词就两 分布式 缓存 分布式 数据库,应用服务器等的多节点部署,数据库的读写分离,剥离文件系统 缓存 数据缓存 静态页面缓存 php时代 最初LAMP起步 并将数据库做读写分离,拆分为主库+从库 ...

  7. 【转】淘宝技术牛p博客整理

    转自:http://blog.csdn.NET/zdp072/article/details/19574793 淘宝技术委员会是由淘宝技术部高级技术人员组成的一个组织,共分为Java分会.C/C++分 ...

  8. 淘宝CDN系统架构

         存储与架构分论坛上,淘宝网技术委员会主席,淘宝网核心工程师章文嵩向我们详细介绍了淘宝网图片处理与存储系统的架构.章文嵩博士的演 讲日程包括了 淘宝的整个系统架构.淘宝图片存储系统架构,淘宝网 ...

  9. “淘宝技术这十年”

    "少时淘气,大时淘宝" 时势造英雄 起因eBay 易趣 在资本方面对仗,阿里想趁此崛起新项目就要求能在短时间内做出一个 个人对个人的商品交易网站(C2C)2003年4月7日-5月1 ...

随机推荐

  1. npm 私有模块的管理使用

    你可以使用 NPM 命令行工具来管理你在 NPM 仓库的私有模块代码,这使得在项目中使用公共模块变的更加方便. 开始前的工作 你需要一个 2.7.0 以上版本的 npm ,并且需要有一个可以登陆 np ...

  2. 应用工具 .NET Portability Analyzer 分析迁移dotnet core

    大多数开发人员更喜欢一次性编写好业务逻辑代码,以后再重用这些代码.与构建不同的应用以面向多个平台相比,这种方法更加容易.如果您创建与 .NET Core 兼容的.NET 标准库,那么现在比以往任何时候 ...

  3. TODO:GitHub创建组织的步骤

    TODO:GitHub创建组织的步骤 使用GitHub进行团队合作,写这个步骤主要作用是为了OneTODO作为一个团队组织进行代码的分享,让更多人来参与. 使用帐号.密码登录GitHub 2.右上角加 ...

  4. node中的cmd规范

    你应该熟悉nodejs模块中的exports对象,你可以用它创建你的模块.例如:(假设这是rocker.js文件) exports.name = function() { console.log('M ...

  5. 旺财速啃H5框架之Bootstrap(一)

    接下来的时间里,我将和大家一起对当前非常流行的前端框架Bootstrap进行速度的学习,以案例的形式.对刚开始想学习Bootstrap的同学而找不着边的就很有帮助了.如果你想详细的学习Bootstra ...

  6. Spark读写Hbase的二种方式对比

    作者:Syn良子 出处:http://www.cnblogs.com/cssdongl 转载请注明出处 一.传统方式 这种方式就是常用的TableInputFormat和TableOutputForm ...

  7. 声音分贝的概念,dBSPL.dBm,dBu,dBV,dBFS

    需要做个音频的PPM表,看着一堆的音频术语真是懵了,苦苦在网上扒了几天的文档,终于有了点收获,下面关于声音的分贝做个总结. 分贝 Decibel 分贝(dB)是一个对数单位(logarithmic u ...

  8. Java中常用集合操作

    一.Map 名值对存储的. 常用派生类HashMap类 添加: put(key,value)往集合里添加数据 删除: clear()删除所有 remove(key)清除单个,根据k来找 获取: siz ...

  9. 全网独家MongoDB Certified DBA Associate考试认证视频

    该视频意在让所有学员一次通过考试,避免重复考试而承担的巨额考试费用! 目前MongDB发展迅猛,有赶超mysql,和oracle看齐的苗头.在这个时候MongoDB也适时的推出了官方的认证考试&quo ...

  10. 算是休息了这么长时间吧!准备学习下python文本处理了,哪位大大有好书推荐的说下!

    算是休息了这么长时间吧!准备学习下python文本处理了,哪位大大有好书推荐的说下!