用于编写更好的 Windows Phone 应用程序的实用技巧
我喜欢为 Windows Phone 编写应用程序。我喜欢针对 Windows Phone 平台创建代码示例和撰写程序,这已成为我的日常工作的一部分,尽管一路走来并不总是一帆风顺。在工作中,我会跌倒了爬起来,陷入迷途,然后又拨云见日。我想要做的就是在本文中提供一些线索,帮助您克服前进道路上的艰难险阻。我将谈到控件、用户数据报协议 (UDP) 多播、广告控件、独立存储、工具和 256MB 设备。因此,请坐下来轻松地欣赏这篇文章。
在使用 HyperlinkButton 时指定 TargetName
HyperlinkButton 控件是用于显示超链接的一种按钮控件。我总是使用这个控件帮助用户在我的应用程序中或 Web 上进行导航。
在我的一个应用程序中,我使用了 HyperlinkButton 显示一些帮助信息,并且希望使用一个超链接,让用户可以在 Web 上获得更详细信息。因此,我写了下面的代码(细节就忽略了):
<HyperlinkButton NavigateUri="http://www.msdn.com">Help</HyperlinkButton>
非常简单,是不是? 我运行了我的应用程序,导航到了包含我的小帮助按钮的页面,然后单击了该链接。 什么也没发生。 之后,我尝试对该应用程序进行调试,并且再次单击了该链接。 这次,系统显示了一个 NavigationFailed 异常,指出:
仅对作为片段、以“/”开头或包含“;component/”的相对 URI 支持导航。参数名称: uri。
开始头痛了吧。 但是,如果更仔细一点阅读文档,这个问题就会迎刃而解。 我尚未对 HyperlinkButton 控件设置 TargetName 属性。 该属性指定超链接的目标窗口。 没有设置该属性意味着我实际上在使用默认值“”,即一个空字符串。 这转化为 HyperlinkButton 尝试将内容加载到单击了其链接的页面中。 这将不会起作用,因为它来自的页面不是浏览器。 我因此做了下面的修改:
<HyperlinkButton NavigateUri="http://www.msdn.com"
TargetName="_blank">Help</HyperlinkButton>
这会将链接的文档加载到一个新的浏览器实例中。现在单击该链接就会起作用了。
RichTextBox 控件也可以使用 HyperLink 内嵌元素显示超链接。这将具有同样的问题。请设置 HyperLink 的 TargetName 属性,否则您会继续头痛!有关可用于 Windows Phone 的控件的详细信息,请参见 wpdev.ms/windowsphonecontrols。
使用 DataContext 在 ListBox 中引用绑定对象
有时候,您将希望在用户点击并按住时执行操作。例如,用户可以点击并按住列表中的某一项。确定哪一项处于被按住状态可能有点难。我将使用一个简化的 ViewModel 进行阐释,如图 1 中所示。
图 1 一个简单的 ViewModel
public class SimpleViewModel : INotifyPropertyChanged
{
private string _myText = "Initial Value";
public string MyText
{
get
{
return _myText;
}
set
{
if (value != _myText)
{
_myText = value;
NotifyPropertyChanged("MyText");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string PropertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this,
new PropertyChangedEventArgs(PropertyName));
}
}
}
我想要按如下所示公开这些项的集合:
public ObservableCollection<SimpleViewModel> Items { get; private set; }
在该页的构造函数中,我实例化这个集合并且添加一些测试数据,如图 2 中所示。
图 2 实例化集合并且在页构造函数中添加测试数据
// Constructor
public MainPage()
{
InitializeComponent();
// Crete some test data
Items = new ObservableCollection<SimpleViewModel>();
Items.Add(new SimpleViewModel
{
MyText = "My first item",
});
Items.Add(new SimpleViewModel
{
MyText = "My second item",
});
Items.Add(new SimpleViewModel
{
MyText = "My third item",
});
this.DataContext = Items;
}
我想要在页面上的列表中显示图 2 中的数据,并且向列出的每一项都添加一个小图形。 我是使用对 ListBox 控件的标准 XAML 绑定来完成上述工作的,如下所示:
<ListBox ItemsSource="{Binding}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Source="duck.png" Height="60" Width="60"/>
<TextBlock Text="{Binding MyText}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
最终结果是一个列表,该列表由具有很可爱图像的多个项构成并且每一项都具有 MyText 值,如图 3 中所示。
图 3 由具有图像的多个项构成的列表
接下来,我想要在用户点击并按住列表中的某一项时弹出一个上下文菜单。 这不是选择某一项,只是点击并按住该项。 我希望该上下文菜单基于当前上下文(也就是说,我正在按下的项的基础数据)进行变化。
为此,我不得不更改 ListBox XAML,以便为 Hold 事件添加 eventHandler,如下所示:
<ListBox ItemsSource="{Binding}" Hold="ListBox_Hold">
我如何确定数据上下文是什么? 这个奇妙的事情在 Hold 事件处理程序中发生了:
private void ListBox_Hold(object sender,
GestureEventArgs e)
{
SimpleViewModel data =
((FrameworkElement)e.OriginalSource).DataContext
as SimpleViewModel;
MessageBox.Show(data.MyText);
}
那么,这里会发生什么呢? 尽管 ListBox 是处理 Hold 事件的控件,但该事件实际上在 ListBoxItem 的 UI 元素之一(例如 Image 或 TextBlock)上触发的。 因为我没有对每个 ListBoxItem 都设置 DataContext,所以它们将从其父 UI 元素继承 DataContext。 因此,通过将 OriginalSource 转换为 FrameworkElement,我们可以访问 DataContext,这是基础数据对象(或者此示例中的 SimpleViewModel)。
不能对蜂窝连接使用多播
蜂窝连接不支持 UDP 多播。 这意味着什么? BeginJoinGroup 开始对 Multicast 组执行联接操作。 如果您在电话只连接到蜂窝网络时调用 BeginJoinGroup,将会出现 SocketException。 您可以反映出这一情况,如图 4 中所示。
图 4 尝试联接 UDP 多播组
try
{
// Make a request to join the group.
_client.BeginJoinGroup(
result =>
{
// Complete the join
_client.EndJoinGroup(result);
// Send or Receive on the multicast group
}, null);
}
catch (SocketException socketException)
{
if (socketException.SocketErrorCode
== SocketError.NetworkDown)
{
MessageBox.Show("UDP Multicast works only over WiFi/Ethernet");
}
}
显然,在真实世界中,与显示 MessageBox 相比,您可以通过更优雅一点的方式来处理此异常。您通过使用 SetNetworkRequirement 扩展方法来改善这个情况。至少您在提前声明您知道正在做什么,尽管在该情况下仍将会收到 NetworkDown 异常。在 wpdev.ms/windowsphonenetworking 中将会提供与 Windows Phone 上的网络有关的详细信息。
控制您的应用程序启动时间
我的第一个应用程序是一个付费应用程序,我向它提供了试用模式,这样,用户在为它掏腰包之前可以试用主要功能。我对下载数量很满意,并且从试用到掏钱购买之间的转换比率也很正常。我需要的就是大下载量!在等待下载量突飞猛进之际,我认为应该尝试提供免费的应用程序,在程序中放置一些广告,看看可否为我的投资带来某种形式的回报。
对于开发人员来说,基于广告的收入模型是将其开发成果转化为金钱的一种很流行的方式。我最初在自己的应用程序开发工作中对是否走这条路还比较犹豫,主要是因为我编写的第一个应用程序是面向年幼的孩子的,我不信任应用程序中针对该年龄段的目标群体的广告。电视上已经有足够的广告给他们看了。我躲避这个模型的另一个原因是我的无知。我不知道如何投放广告,并且不知道如何在应用程序中实现广告。现在回想,确实不应该如此胆小。使您的应用程序基于广告并且了解印记、有效每千次印记费用 (eCPM) 和广告单元轻而易举,而 Microsoft Advertising SDK for Windows Phone 就是一个光辉榜样,使用它您就会发现投放广告是如此容易。
Microsoft Ad Control for Windows Phone 内置于 Windows Phone SDK 中,使用它您可以立即投放广告。您只需在 Pub Center 中创建一个应用程序 ID 和广告单元,将广告控件拖到页面中,将该控件中的 ApplicationId 和 AdUnitId 属性设置为您从 Pub Center 获取的值,这就够了。您可以在 wpdev.ms/adsdk 上找到详细信息。尽管 Advertising SDK 是 Windows Phone SDK 随附的,但一定要在 wpdev.ms/adsdkupdates 上检查是否有 Advertising SDK 的更新,因为它们可能会独立提供。
但是,与所有这些能力随之而来的还有责任。您想要确保对于所有 UI 元素,加载广告控件不会影响您的应用程序的响应性,特别是不会影响应用程序启动时间。在某个人点击您的应用程序图块时,您希望在其改主意之前不会离开您的应用程序。
在 Visual Studio IDE 中内置的市场测试工具包就是测试您的应用程序的启动时间的一个简单方法。使用该工具包中的监视测试,您可以在电话上启动您的应用程序,体验该应用程序,然后关闭该应用程序。该工具包然后对在您的应用程序运行时收集的度量值进行分析,向您提供有关启动时间、峰值内存使用情况等的反馈。如果在您提交应用程序时未能通过上述任何测试,则无法通过认证。针对 Windows Phone 的技术认证要求包括针对应用程序启动时间的要求。在撰写本文时,针对启动时间的要求是,应用程序必须在启动后的 5 秒内呈现第一个屏幕。若要查看认证要求的最近的列表,请参见 wpdev.ms/techcert。
请始终监控您的启动时间,确保应用程序不会超过 5 秒的限制。将广告控件添加到您的应用程序的主页也可能会影响启动时间。页面上出现的任何内容都会影响启动时间,但广告控件可以很好地阐释这一影响。幸运的是,有方法可以避免广告控件之类的控件影响您的启动时间,只要您不介意在第一个广告在页面上出现时有些许延迟。我并不介意,因为我的应用程序的响应性以及良好的用户体验在任何时候都比多一点广告印记重要。我采用的模式就是延迟广告控件的加载。既然我讲到它了,我还是要通过处理可能会导致的错误(由于广告因网络问题而未能加载或没有实际要加载的广告等),让我的代码更强健。
我将最终将包含该广告控件的网格放置于我的页面上,而不是将广告控件放置于页面内的 XAML 中:
<!—Ad Control is added dynamically in codebehind-->
<Grid Grid.Row="0" Height="90" x:Name="AdGrid"/>
在该页的代码隐藏文件中,我向用于该广告控件的类添加了一个成员变量(不要忘了在您的项目中添加针对 Microsoft.Advertising.Mobile.UI 的引用):
// Ad Control to add dynamically to AdGrid
private AdControl adControl = null;
在该页的构造函数中,我挂接了已加载的事件,如下所示:
public MainPage()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(MainPage_Loaded);
}
在 MainPage_Loaded 方法中,我添加了以下代码:
// Load Ad Control in Loaded event, so it won't count against the app's launch time
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
LoadAdControl();
}
LoadAdControl 方法将用于向我在 XAML 中创建的 AdGrid 添加新的广告控件,如图 5 中所示。
图 5 以编程方式向网格中添加广告控件
private void LoadAdControl(){
// Create Ad Control if it doesn't exist already
if (adControl == null || !AdGrid.Children.Contains(adControl))
{
adControl = new AdControl(APPID, ADUNITID, true)
{
Width = 480,
Height = 80,
VerticalAlignment = System.Windows.VerticalAlignment.Top
};
// Hook up some interesting events
adControl.AdRefreshed += new EventHandler(adControl_AdRefreshed);
adControl.ErrorOccurred +=
new EventHandler<Microsoft.Advertising.AdErrorEventArgs>(
adControl_ErrorOccurred);
// Add it to the AdGrid
AdGrid.Children.Add(adControl);
}
}
请注意,图 5 中的 APPID 和 ADUNITID 应被您在 Pub Center 上创建广告单元时获取的 ID 替换。
您将注意到,我还在此方法中挂接了 AdRefreshed 和 ErrorOccurred 事件。 为处理这些事件做好准备是一种很好的做法。 在通过网络检索广告时可能会出现错误,处理好这些错误情况十分重要。 在本例中,我只是希望显示文本的一部分,例如我的应用程序的名称。 其他人胆子更大,实际上使用了针对其他 Windows Phone Marketplace 的广告或来自其他提供商的广告填充了这个位置。 图 6 显示了我是如何处理广告控件错误的。
图 6 处理广告控件错误
// If there's an error, display some text
void adControl_ErrorOccurred(object sender,
Microsoft.Advertising.AdErrorEventArgs e)
{
AdControl ad = (AdControl)sender;
Dispatcher.BeginInvoke(() =>
{
// Hide the Ad Control
ad.Visibility = System.Windows.Visibility.Collapsed;
// Place something in its place.
I chose to add a TextBlock
// containing the name of my app.
You could instead instantiate
// an ad control from another provider, show static ads for your other
// apps or do nothing.
if (tbBrand == null)
{
tbBrand = new TextBlock()
{
Text = "My Application",
Foreground = (Brush)Resources["PhoneForegroundBrush"],
FontSize = (double)Resources["PhoneFontSizeMedium"],
HorizontalAlignment = System.Windows.HorizontalAlignment.Center,
VerticalAlignment = System.Windows.VerticalAlignment.Center,
Margin = new Thickness(10)
};
}
If (!AdGrid.Children.Contains(tbBrand))
{
AdGrid.Children.Add(tbBrand);
}
tbBrand.Visibility = System.Windows.Visibility.Visible;
});
}
我添加了 AdRefreshed 处理程序,因为我想要确保在出现错误后再次运行该广告控件后是可见的(参见图 7)。
图 7 添加 AdRefreshed 处理程序
// Make sure the Ad Control is visible if the ad was refreshed
void adControl_AdRefreshed(object sender, EventArgs e)
{
AdControl ad = (AdControl)sender;
Dispatcher.BeginInvoke(() =>
{
ad.Visibility = System.Windows.Visibility.Visible;
if (tbBrand != null)
{
tbBrand.Visibility = System.Windows.Visibility.Collapsed;
}
});
}
这是很好的行为并且使我的广告控件的使用更为强健,但它对启动时间有什么影响呢?
为了加以说明,我使用“文件”|“新建项目”|“Windows Phone 应用程序”创建了一个新项目。我首先将一个广告控件直接放入 XAML 中。然后,我运行了市场测试工具包,并且将应用程序部署到了我的电话上。我运行了我的小应用程序多次,并且观察了相应的启动时间。平均时间为 2.7 秒。这仍是可接受的启动时间。接下来,我应用了前面介绍的延迟加载模式。我再次通过市场测试工具包部署了我的设备并且进行了测试,测试次数与我在第一个测试案例中所执行的次数相同。平均启动时间现在仅为 1.9 秒。这个话题就说到这里了。
使用 IsolatedStorageSettings 存储简单设置
我对 IsolatedStorageSettings 十分偏爱,并将它作为存储应用程序设置的首选方法。就其键值对结构而言,它应该仅用于最简单的设置。我通常使用它来存储一些设置,例如用户在我的应用程序中启用还是禁用了声音、要在每一页上显示的对象数、最后更新数据的时间等。这里应存储的是简单数据,并且它确实易于使用。请在 wpdev.ms/settingspage 处的 MSDN 库文档中查看说明如何创建您自己的设置页的过程。一旦您尝试了之后,我相信您会被迷住的。
在要开始存储大量数据时,不可避免要写入处于独立模式下的文件。请记住对启动时间的要求,您应该遵守这些指导原则以便以异步方式加载数据(BackgroundWorker, 一个异步 Web 调用;或者当前的任何异步调用)。我还采用了仅将增量数据存储到磁盘的模式,并且只要增量数据出现,基本上我就是这样做的。有关 Windows Phone 上的状态的详细信息,请查看 bit.ly/oR96Ux。
不要对不存在的文件使用 FileMode.Append
如前所述,应该给予很大的关注以便优化与独立存储之间的数据传输。您的应用程序的响应性至关重要 — 否则,您会发现您孜孜以求的应用程序下载是如此之难,好像全世界每个用户都在卸载它。在我的作品之一中,我已能够相当快速地存储和加载我的数据,直到我发现有一些不该发生的事情发生了。我正在丢失一些数据!出于许多原因我没有发现这个情况。一个原因是我在一个深夜放入某个 catch 块中的“失散多年”的 Debug.WriteLine 语句。所以,我就在那里考虑对独立存储写点什么,而错误列表总是会让我疯掉。我认为我正在向一个文本文件附加数据,但错误列表传递以下消息:
不允许对 IsolatedStorageFileStream 执行操作。
我对在本文这样的文章中显示错误代码没什么热情。 我养成这个习惯可以追溯到我高中时的数学老师,那是个袜套、“家庭关系”和 Commodore 64 风靡一时的时代。 是的,他永远不会显示问题的错误回答,以免陷入这个错误中。 (题外话: 我的老师姓名的首字母缩写是 D.O.S.,这真够怪的 — 我想 DOS 不会是来自这个出处吧!) 因此,下面是我应该一直在做的。
永远不要尝试对不存在的文件调用 FileMode.Append。 创建文件,写入该文件,然后用以后传输的内容追加该文件,如图 8 中所示。
图 8 写入处于独立存储下的文件
if (!_store.FileExists("mydata.data"))
{
// Open for writing
using (var textFile =
_store.OpenFile("mydata.data", FileMode.CreateNew))
{
WriteRecordsToFile(textFile, dataString);
}
}
else
{
using (var textFile = _store.OpenFile("mydata.data",
FileMode.Append))
{
WriteRecordsToFile(textFile,dataString);
}
}
private void WriteRecordsToFile(IsolatedStorageFileStream textFile,
string data)
{
using (var writer = new StreamWriter(textFile))
{
writer.WriteLine(data);
}
writer.Flush();
writer.Close()
}
使用 Isolated Storage Explorer 在您的应用程序中测试存储
为 Windows Phone 开发工具的势头正猛,并且我让我激动不已的是,一些高质量的工具正在填补 Microsoft 所提供的产品中的空白。但要提醒您的是,这些空白并不是什么大问题,在针对其他移动平台的其他工具集上您也会发现同样的空白。Microsoft 只是没找到时间来填补这些空白。例如,Microsoft 通过提供 Isolated Storage Explorer 命令行工具来取得进展。在 Microsoft 外部您会发现围绕此工具开发的许多包装,可给您带来很大的方便。一个此类工具就是 Windows Phone 7 Isolated Storage Explorer,您可以在 CodePlex (wp7explorer.codeplex.com) 上找到该工具。浏览并找到适合您的工具,或者坚持使用命令行。但探索一下独立存储吧,例如使用工具将测试数据放入您的应用程序的独立存储中。这比在您已交付后发现这些错误要容易得多。
使用市场测试工具包
市场测试工具包是使您在开发应用程序时保持控制的一个很棒的工具。直接从 Visual Studio IDE 内运行该工具,它提供一组执行有用检查的测试,例如验证您具有针对您的应用程序的正确插图、检测您的应用程序正在使用的实际电话功能、报告您的应用程序的启动时间等。甚至有一些手动测试也可以很好地指出在应用程序认证过程中所看到的情形。这是每个开发人员都想在某个晚上搞定并在 CodePlex 上发布供每个人使用的功能之一。Microsoft 走在了前面,希望这意味着针对此工具包有一个路线图,该工具包将在此基础上建立,并沿着前进道路越走越远。
除了一次未通过认证,我通过了每个应用程序认证,那次失败是因为错误地理解了 Windows Phone Marketplace 测试团队测试我的一个应用程序的方式。我可以把这个算在我自己的天分或勤奋上,但实际上还有 Marketplace Test Kit 的一份功劳,您应该也可以利用它的。
如何我还有什么要抱怨的话(并且这也是要提醒您注意的),就是在您尝试对仿真器运行监视测试时,测试失败有些悄无声息。这个失败只是在您单击“开始应用程序”将您从图 9 带到图 10,并且将仿真器选择为目标设备。
图 9 运行监视测试前
图 10 监视测试失败
我认为这种通知有点太悄无声息了,以致您只能对真正的电话运行这些监视测试。我的意见是,如果有关此失败的通知能够更显眼些,那就更好了。您可以在 wpdev.ms/toHcRb 阅读有关市场测试工具包的详细信息。
使用 WPConnect 测试您的媒体应用程序
通常,在我开始胡乱地修改媒体应用程序时,我会有几个小时乱搞一气,但在我的电话连接到 PC 时我的首次测试运行指出无法查看媒体库而让我很恼火后,立刻就放弃摆弄了。那么,我会转到平台的让我感觉更舒服的区域,摇晃着脑袋感叹这些媒体应用程序开发人员真是太有耐心了。在我发现 Windows Phone Connect Tool (WPConnect) 之前就是这样。是的,有一段时间一直是这种情况,但当我在文档中偶然发现了这个“金块”后,真令我喜上眉梢。它是干什么用的?简单地说,它使您能够在连接到您的 PC 时调试媒体应用程序。上述说明非常简单,因此,请访问 wpdev.ms/wpconnect 来了解详情。
测试 256MB 设备的响应性
有的电话具有 256MB 的 RAM。由于其内存容量较小,因此您应该监视自己的应用程序的内存占用量,并且决定要在这些电话上使用您的应用程序时做什么。您可以决定检测某一主机设备是否为 256MB 设备,以及是否关掉您的应用程序上一些花里胡哨的东西。您也可以得出结论,您的应用程序就不是为这些设备编写的,您宁愿接受您的应用程序不会在这些设备上出现的打击,也不愿冒由于您的应用程序的明显性能问题而得到一星评分的风险。
使用 Windows Phone SDK 7.1.1,Microsoft 将帮助您在此情况下作出明智的决定。您可以查看主机设备的 ApplicationWorkingSetLimit 所提供的信息,确定电话是否是 256MB 类别的设备。您甚至可以选择使您的应用程序在 Windows Phone Marketplace 中对于这些设备不可见。请查看 wpdev.ms/256devices 上的详细信息。
在本文中,我向您介绍了我在编写应用程序时发现的许多问题。并且还重点介绍了给我很大帮助的一些工具。现在应该很清楚了,应用程序响应性是在编写应用程序时要秉承的核心价值。我希望其中某些内容在您编写 Windows Phone 应用程序时会对您有所帮助。Windows Phone 是一个强大的平台,我仍热爱为其做点或写点什么。我每天都很喜欢浏览 Windows Phone Marketplace,欣赏像您这样富有创造性的人士源源不断提供的所有优秀的新作品。如果我尚未有机会试用您的应用程序,我期待能有这样的机会。
Andrew Byrne 是 Windows Phone 团队中的一名高级程序员。 他对软件的知识和热情来自于他在软件开发行业的 21 年,在这 21 年中,他为许多国际性组织工作,也曾自己创业。
衷心感谢以下技术专家对本文的审阅: Kim Cameron、Mark Hopkins、Robert Lyon、Nitya Ravi、Cheryl Simmons 和 Matt Stroshane