很久没有更新博客了.最近一直陷身在项目中难以有时间抽身梳理总结.关于博客确实很多想写的主题.节前大概草草 的梳理一下大概就有十几个主题.只能趁着放假的时间来逐渐把这批文章力所能及系统的更新出来. 主要涉及到我们团队现在Windows phone 项目开发中实际碰到一些问题和对应解决方案.如果想关注即时了解每天动态信息可以直接在Sina微博@chenkaiHome 沟通交流.

在开始更新这批博文前.一直在顾虑先更新那个主题为好.回头一想索性就说说这半个月有些苦恼的Windows phone中处理 WebBrowser在我们项目中表现出来问题.

话说去年.技术团队提出要优化产品在各个平台[IOS/Android/WP7/QT]客户端开发业务流程.提出这个问题主要是为了把原来通用业务逻辑流程封装到能力更大的服务器端来做.各个客户端在通过WebView统一的形式调用.这样做目的.主要是解决原来各个客户端在业务升级后更新客户端版本时减少重新开发量.这样一来把核心的业务逻辑变动全部集中服务器端.需求变化自上而下传递过程中.在各个平台之间可以复用. 在每次更迭客户端版本时提升开发团队效率.

最开始我们采用的方案焦点主要是考虑到WebView封装通用的业务逻辑流程涉及到与对应平台原生应用程序的交互问题上.所以也就理所当然有人提出跨平台移动框架PhoneGap[平台交互]+HTML 5[UI呈现]的处理方案. 原来我们设想真的很简单.也天真的认为PhoneGap+HTML 5搭配会把通用业务流程问题迎刃而解.

首先.PhoneGap[PS参考:Windows phone 应用开发[8]-体验PhoneGap]作为移动跨平台框架.着重解决的问题是通过JavaScript实现跨平台API交互.并没有UI.页面还需要借助HTML 5效果.即使如此.也是难以和原生应用程序界面相媲美的.这就需要在使用过程中.要牺牲掉大量原生应用程序交互细节.用户体验上打了一个折扣.当然这和我们解决的核心问题做出牺牲还是值得.但问题各个平台兼容性需要调整各个平台适配问题大大出乎我们预估.而在性能差异上更是难以兼顾保证的.而对于初次使用PhoneGap团队解决这些问题所耗费的开发周期时间.却远大于开发Native Application原生应用时间还要长.这完全和使用初衷相背离. 其实问题只是换了一种形式存在. 我们只是从一个熟悉能够预估量的泥潭跳到另外完全未知泥潭中.不断尝试挣扎…

谈到这.说一个细节.类似在WebBrowser浏览器控件中.打开一个新窗口的问题.Android平台可以采用LoadUrl方法直接打开一个新窗体实现Js控制的页面跳转. 而目前Windows phone WebBrowser情况不支持在当前页打开新窗体.虽然可以通过InvokeJavaScript()放在LoadComplated方法中注入Js控制方法替换打开方式来实现. 但在实际调试中会发现.开发人员在C# 后台代码中调试JavaScript来说是一个挑战. 一来WebBrowser在注入和执行Js过程返回的错误或异常都是简单的代码80开头Code.而没有具体的堆栈信息. 这对找错和确认问题照成很大障碍.另外在C#后台代码处理JS缺乏有效的调试工具支持.这对PhoneGap封装出来页面复杂的Js调用或数据交互操作.照成一定难题.

说白了.在PhoenGap中通过JS实现Windows phone应用平台交互主要体现在两个点上.第一就是通过在Js中调用:

【JAvaScript:】

window.external.Notify(“”);

方法把页面交互数据通过WebBrowser控件SCriptNotify事件接收传递给原生应用程序. 另外一个点.就是可以在在Windows phone应用程序直接调用WEbBrowser控件InvokeScript()方法来调用JavaScript函数. 这两个方法.实现了PhoneGap数据传递和交互整个过程.

2012-04-03_233226

那在Windows phone应用程序使用WebBrowser有哪些常见需要解决的问题?

[1]异常处理.

well.这里不得不首先说在WebBrowser调用JavaSCript时需要处理的异常问题.Windows Phone 提供一个基于桌面版本的 Silverlight 的 WebBrowser 控件,也就是说WindowsPhone 目前的WEbBrowser控件是基于Silverlight桌面版本的WebBrowser控件而来,但仍然有几处不同[WEbBrowser与Silverlight版本不同地方].其中两个版本在开发最大不同主要有如下几点:

Windows phone WebBrowser控件与Silverlight 桌面版本的不同:

[1]Windows phone 版本相对Silverlight版本具有直接使用 IsolateStroage独立存储的权限

[2]相对Silverlight在执行InvokeScript()方法时限制了执行范围必须是XAP 程序包相同的站点中加载的脚本.而Windows phone 解除该限制.

[3]在Windows phone版本时从独立存储加载的内容或使用 NavigateToString(String) 方法加载的内容没有跨站点访问限制。

那么在Windows phone WebBrowser中调用JavaScript常见的可能出现的异常主要有两个,如下重现这两个异常情况.首先创建一个Windows Phone Application 应用程序. Mainpage.CS:

1: <!--ContentPanel - place additional content here--> 2: <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> 3: <StackPanel> 4: <phone:WebBrowser x:Name="ComponentContent_WB" Height="450"/> 5: <Button x:Name="ExcuteScript_BT" Content="Excute JavaScript" Margin="0,50,0,0" Click="ExcuteScript_BT_Click"></Button> 6: </StackPanel> 7: </Grid>
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

定义一个WebBrowser和Button按钮用来在页面加载完成后执行JavaSCripti函数事件.当然在执行JavaSCript需要设置WEbbrowser可以调用JS的. 设置IsScriptEnabled="True" BehindCode 如下:

1: // Constructor 2: public MainPage() 3: { 4: InitializeComponent(); 5: this.Loaded += new RoutedEventHandler(MainPage_Loaded); 6: } 7:   8: void MainPage_Loaded(object sender, RoutedEventArgs e) 9: { 10: string navigateUrl = @"http://www.163.com"; 11: this.ComponentContent_WB.Navigate(new Uri(navigateUrl, UriKind.RelativeOrAbsolute)); 12: } 13:   14: private void ExcuteScript_BT_Click(object sender, RoutedEventArgs e) 15: { 16: //Button Client Event Excute InvokeJavaScript Method 17: try 18: { 19: this.ComponentContent_WB.InvokeScript("DefineNoExistJSMethod"); 20: } 21: catch (Exception se) 22: { 23: MessageBox.Show("Excute JavaScript Have Exception:" + se.Message); 24: } 25: }
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

调用通用网易站点.在加载页面完成后通过Button按钮执行一个不来就不存在JavaScript函数.执行效果如下":

2012-04-04_151451

因这个JavaScript函数不存在所以执行肯定报错.注意这里报错信息是以80020006为开头的UnKnowError如下:

2012-04-04_152050

可见在堆栈的异常信息一栏中对JavaScripit提供的信息非常有限.这个Message代码为80020006.其实就是在当前应用程序执行范围找不到该JavaScript方法.另外一种情况恰恰相反.在执行已经定义JavaScript Function 函数出现的异常. 类似找到163.com站点中一个任意JAvaScript函数在后台方法调用:

1: function NTESAutoComplete ( inputElem, nextElem ) { 2: var t = this; 3: t._inputElem = inputElem; 4: t._nextElem = nextElem; 5: t._idName = "login_auto_list"; 6: t._className = "login-auto-list"; 7: }
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

注意InvokeScript方法在执行带有JAvaScript参数时. 参数传递是以String[]数组方式传递给JAvaScript函数.调用:

1: private void ExcuteScript_BT_Click(object sender, RoutedEventArgs e) 2: { 3: //Button Client Event Excute InvokeJavaScript Method 4: try 5: { 6: this.ComponentContent_WB.InvokeScript("NTESAutoComplete",new string[]{"NoExistElement", "NoExistStringArgument"}); 7: } 8: catch (Exception se) 9: { 10: MessageBox.Show("Excute JavaScript Have Exception:" + se.Message); 11: } 12: }
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

执行效果如下:

2012-04-04_162830

执行过程中InvokeScript得到异常 "An unknown error has occurred. Error: 80020101".而这个异常是在往往执行过程JavaScript内部错误引起.因在后台代码没有有效的工具.支持.所以对于JavaScript的错误是很难查找确认问题具体在那. 这个问题出现一般会有两种大概原因.

第一点.在调用InvokeScript()是WebBrowser控件事件执行顺序.其实针对WEbBrowser控件.除了从FrameworkElement类和Control类继承了通用了UIElement属性和方法外.WEbBrowser重点扩展自身导航操作.类似其中三个比较中重要的方法.Navigating、Navigated 和 LoadCompleted事件.

那么说到这 这个三个事件在实际操作执行顺序是?

WebBrowser导航事件的执行顺序:

Navigating &gt; Navigated &gt; LoadCompleted

Navigating是执行Navigate方法表示当前WEbBrowser正在执行加载URL操作、Navigated事件WebBrowser 控件成功导航后发生 和 LoadCompleted事件在 WebBrowser 控件成功加载内容后发生.

如果在这种情况下.即使我们发现我们Codebehind中InvokeScript()调用JS没有问题.同时HTML JavaScript函数测试也没有问题.这就导致我们始终无法通过程序测试找到JS 报错80020101异常在那. 这是在后台代码调试JAvaScript最让人痛苦的地方.比如我们在如下方法掉用如上JavaScript函数:

void Wb_Navigated(object sender, System.Windows.Navigation.NavigationEventArgs e) { Wb.InvokeScript("eval", "document.forms[0].submit();"); // Throws 80020101 }   private void MainPage_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { Wb.InvokeScript("eval", "document.forms[0].submit();"); // Works }
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

可以发现.如果在Navigated事件触发时.即使我们后台代码调用和JavaScript函数都没有错误.依然还是爆出80020101的异常.这主要是因为DOM对象操作在页面触发Navigated事件还没有完全初始化.导致调用页面执行时出现异常.

第二点.则是比较常见的即使需要对JavaSCript做一定修改.确保JS函数在执行时不会出错. 则这个80020101异常一般都会在如上两种情况下出现.

[2]现实静态页面.

在WEbBrowser中.可能需要在没有网络情况下.需要在某一些情况下通过后台应用程序操作HTML页面. 而在Windows phone提供两种方式来加载本地静态的HTML页面.

在Windows phone 中WEbBrowser中提供NavigateToString方法将 HTML 字符串置于 Web 浏览器控件中以便进行呈现.操作也是简单的:

1: string defineHtmlStr = @"<html> 2: <head> 3: <script> 4: function DefineExistFun(elementStr) 5: { 6: var getElems=document.getElementByTag(elementStr); 7: alert(elementStr); 8: } 9: </script> 10: <body> 11: <a href=" + "http://chenkai.cnblogs.com" + ">Test</a>" 12: + "</body>" 13: + "</head></html>"; 14: this.ComponentContent_WB.NavigateToString(defineHtmlStr);
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

加载页面效果:

2012-04-04_171035

另外一种方式.则是加载一定定制好静态HTML页面.使用 WebBrowser 控件在应用程序中显示已设置格式的静态内容。例如,开发人员可能希望在应用程序包中包含帮助文本,以便用户可以随时访问.创建一个静态HTML界面:

1: <html> 2: <head> 3: <script> 4: function DefineExistFun(elementStr) 5: { 6: var getElems=document.getElementByTag(elementStr); 7: alert(elementStr); 8: } 9: </script> 10: <body> 11: <a href="http://chenkai.cnblogs.com">Test</a> 12: </body> 13: </head> 14: </html>
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

在执行第一步需要把该CreateProduct.html页面添加解决方案.设置引用资源为Content.需要向独立存储中添加存储静态文件.:

1: private void SaveFilesToIsoStore() 2: { 3: //These files must match what is included in the application package, 4: //or BinaryStream.Dispose below will throw an exception. 5: string[] files = { 6: "CreateProduct.html" 7: }; 8:   9: IsolatedStorageFile isoStore = IsolatedStorageFile.GetUserStoreForApplication(); 10:   11: if (false == isoStore.FileExists(files[0])) 12: { 13: foreach (string f in files) 14: { 15: StreamResourceInfo sr = Application.GetResourceStream(new Uri(f, UriKind.Relative)); 16: using (BinaryReader br = new BinaryReader(sr.Stream)) 17: { 18: byte[] data = br.ReadBytes((int)sr.Stream.Length); 19: SaveToIsoStore(f, data); 20: } 21: } 22: } 23: } 24:   25: private void SaveToIsoStore(string fileName, byte[] data) 26: { 27: string strBaseDir = string.Empty; 28: string delimStr = "/"; 29: char[] delimiter = delimStr.ToCharArray(); 30: string[] dirsPath = fileName.Split(delimiter); 31:   32: //Get the IsoStore. 33: IsolatedStorageFile isoStore = IsolatedStorageFile.GetUserStoreForApplication(); 34:   35: //Re-create the directory structure. 36: for (int i = 0; i &lt; dirsPath.Length - 1; i++) 37: { 38: strBaseDir = System.IO.Path.Combine(strBaseDir, dirsPath[i]); 39: isoStore.CreateDirectory(strBaseDir); 40: } 41:   42: //Remove the existing file. 43: if (isoStore.FileExists(fileName)) 44: { 45: isoStore.DeleteFile(fileName); 46: } 47:   48: //Write the file. 49: using (BinaryWriter bw = new BinaryWriter(isoStore.CreateFile(fileName))) 50: { 51: bw.Write(data); 52: bw.Close(); 53: } 54: }
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }把需要展示的静态HTML页面在调用前需要存储到独立存储中.调用如下:

1: void MainPage_Loaded(object sender, RoutedEventArgs e) 2: { 3: SaveFilesToIsoStore(); 4: ComponentContent_WB.Navigate(new Uri("CreateProduct.html", UriKind.Relative)); 5: }
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }成功加载的页面:

2012-04-04_171035

.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }针对在Windows phone WEbBrowser中于JavaScript交付问题的问题出现的异常.在后台代码上处理是非常弱的.首先在CodeBehind中没有成行JS调试工具支持.这对不熟悉前段JavaSCript代码的开发人员来说是一个挑战. 另外一个问题就是一旦调用JavaScript出现异常情况.很难确认问题源头.这也大大影响开发效率.

当然在WEbBroser还涉及到页面加载控制. 新窗口打开. 控制WEbBrowser页面缩放等问题.这里就不再一一赘述.

关于本片源码详见:https://github.com/chenkai/WebBrowser-Case-Windows-phone-Sample

源码下载:/Files/chenkai/WebBrowserWP7Demo.rar

如有问题可以Weibo上沟通交流:http://weibo.com/chenkaihome