雖然新的Executing Model加入Dormant讓Windows Phone應用程式進入類多工的模式,但其仍然不是真的完全多工,因為在我們固有印象中的多工,應該是應用程式於背景持續執行,也就是類似常駐程式一樣的效果。
而Dormant模式則是將應用程式暫止,此時該應用程式是被完全暫停,這與舊有印象中的常駐程式相距甚遠。
因此,Windows Phone 7.1加入了Background Agents機制,其細分為兩類,一個是Scheduled Notifications,用來撰寫Reminder(提醒)及Alarm(鬧鐘)類的應用程式,另一個是Scheduled Tasks,用來撰寫需要定時呼叫
的常駐程式,例如收集GPS資料或是於特定時間週期更新網路資料。
Windows Phone 7.1 Overview (2) – Background Agents
文/黃忠成
Background Agents
雖然新的Executing Model加入Dormant讓Windows Phone應用程式進入類多工的模式,但其仍然不是真的完全多工,因為在我們固有印象中的多工,應該是應用程式於背景持續執行,也就是類似常駐程式一樣的效果。
而Dormant模式則是將應用程式暫止,此時該應用程式是被完全暫停,這與舊有印象中的常駐程式相距甚遠。
因此,Windows Phone 7.1加入了Background Agents機制,其細分為兩類,一個是Scheduled Notifications,用來撰寫Reminder(提醒)及Alarm(鬧鐘)類的應用程式,另一個是Scheduled Tasks,用來撰寫需要定時呼叫
的常駐程式,例如收集GPS資料或是於特定時間週期更新網路資料。
Reminder
顧名思義,Reminder指的就是應用程式可指定一段時間週期,由OS負責排程並在排定時間以訊息框方式提醒使用者,這與Windows Phone 7內建的行程管理軟體相似,唯一不同是應用程式可自行定義提醒框中的訊息及週期,
也可定義當使用者看到提醒框後點選後導向應用程式的哪一個頁面,圖3為本節範例執行畫面。
圖3
設定好後,按下Add並離開程式,當時間到時會出現圖4的畫面。
圖4
當點選提醒框後,會導向應用程式指定的頁面。
圖5
撰寫這樣的範例並不難,Reminder其實是一個物件,有以下關鍵屬性:
屬性 |
說明 |
BeginTime |
排程起始時間 |
ExpirationTime |
排程結束時間(於上圖對應為Expired Time) |
Recurrence |
觸發週期: None – 只觸發一次,也就是由BeginTime時間到時觸發 Daily – 每天到BeginTime時間時觸發一次 Weekly – 每周到BeginTime所指的星期幾及時間觸發一次, Monthly – 每月到BeginTime所指的日期及時間觸發一次 EndOfMonth – 每月底到BeginTime所指的時間觸發一次, Yearly – 每年到BeginTime所指的月份、日期、及時間時觸發一次 |
Title |
當提醒框出現時顯示的Title |
Content |
當提醒框出現時顯示的Content |
NavigationUri |
當使用者點選提醒框時,要開啟本程式中的哪個頁面。 |
在本例中,NavigationUri是程式內定的,在指定NavigationUri時可以順便帶入參數,本例中即帶入了兩個參數。
MainPage.xaml |
<phone:PhoneApplicationPage x:Class="DemoReminder.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone" xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"d:DesignWidth="480"d:DesignHeight="768" FontFamily="{StaticResourcePhoneFontFamilyNormal}" FontSize="{StaticResourcePhoneFontSizeNormal}" Foreground="{StaticResourcePhoneForegroundBrush}" SupportedOrientations="Portrait"Orientation="Portrait" shell:SystemTray.IsVisible="True"xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"Loaded="PhoneApplicationPage_Loaded">
<!--LayoutRoot is the root grid where all page content is placed--> <Gridx:Name="LayoutRoot"Background="Transparent"> <Grid.RowDefinitions> <RowDefinitionHeight="Auto"/> <RowDefinitionHeight="*"/> </Grid.RowDefinitions>
<!--TitlePanel contains the name of the application and page title--> <StackPanelx:Name="TitlePanel"Grid.Row="0"Margin="12,17,0,28"> <TextBlockx:Name="ApplicationTitle"Text="Reminder Demo" Style="{StaticResourcePhoneTextNormalStyle}"/> </StackPanel>
<!--ContentPanel - place additional content here--> <StackPanelx:Name="ContentPanel"Grid.Row="1"Margin="12,0,12,0"> <TextBlockHorizontalAlignment="Stretch" Name="textBlock1" Text="Begin Time"VerticalAlignment="Top" /> <StackPanelOrientation="Horizontal"> <toolkit:DatePickerx:Name="ctBeginDate" /> <toolkit:TimePickerx:Name="ctBeginTime" /> </StackPanel> <TextBlockHorizontalAlignment="Stretch" Name="textBlock2" Text="Expired Time"VerticalAlignment="Top" /> <StackPanelOrientation="Horizontal"> <toolkit:DatePickerx:Name="ctExpiredDate" /> <toolkit:TimePickerx:Name="ctExpiredTime" /> </StackPanel> <TextBlockHorizontalAlignment="Stretch" Name="textBlock3" Text="Recurrence"VerticalAlignment="Top" /> <toolkit:ListPickerx:Name="ctRecurrence"ListPickerMode="Normal">
</toolkit:ListPicker> <TextBlockHorizontalAlignment="Stretch" Name="textBlock4" Text="Title"VerticalAlignment="Top" /> <TextBoxHeight="71"Name="ctTitle"Text=""Width="460" /> <TextBlockHorizontalAlignment="Stretch" Name="textBlock5" Text="Content"VerticalAlignment="Top" /> <TextBoxHeight="71"Name="ctContent"Text=""Width="460" /> <TextBlockHorizontalAlignment="Stretch" Name="textBlock6" Text="Params"VerticalAlignment="Top" /> <StackPanelOrientation="Horizontal"> <TextBoxHeight="71"Name="ctParam1"Text=""Width="200" /> <TextBoxHeight="71"Name="ctParam2"Text=""Width="200" /> </StackPanel> <ButtonHorizontalAlignment="Right"Content="Add"Height="71" Name="btnAddOrRemove"Width="160"Click="btnAddOrRemove_Click" /> </StackPanel> </Grid> </phone:PhoneApplicationPage> |
MainPage.xaml.cs |
|
讓我稍微解釋一下這個範例,當使用者設定了各個參數按下Add按鈕後,應用程式會建立一個Reminder物件,然後透過ScheduledActionService來要求OS排入此Reminder,
當Reminder排入後,應用程式便可透過ScheduleActionService來取得已經排入的Reminder,此處以此來判斷而不重複排入同一個Reminder。
注意,Reminder的Name是不能重複的,而ScheduleActionService只能夠取得本應用程式所排入的Reminders,無法取得其它應用程式排入的Reminders。
當Reminder加入ScheduleActionService後,該Reminder即處於排程狀態,當指定的時間到時,不管使用者是否正在執行本應用程式或是正處於其它應用程式中,OS都會秀出
提醒框來提示使用者,當使用者點選提醒框時,OS會啟動排入此Reminder的應用程式,並開啟NavigationUri所指定的頁面,下面為此範例所指定的ShowReminder.xaml原始碼。
ShowReminder.xaml |
<phone:PhoneApplicationPage x:Class="DemoReminder.ShowReminder" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone" xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" FontFamily="{StaticResourcePhoneFontFamilyNormal}" FontSize="{StaticResourcePhoneFontSizeNormal}" Foreground="{StaticResourcePhoneForegroundBrush}" SupportedOrientations="Portrait"Orientation="Portrait" mc:Ignorable="d"d:DesignHeight="768"d:DesignWidth="480" shell:SystemTray.IsVisible="True"Loaded="PhoneApplicationPage_Loaded">
<!--LayoutRoot is the root grid where all page content is placed--> <Gridx:Name="LayoutRoot"Background="Transparent"> <Grid.RowDefinitions> <RowDefinitionHeight="Auto"/> <RowDefinitionHeight="*"/> </Grid.RowDefinitions>
<!--TitlePanel contains the name of the application and page title--> <StackPanelx:Name="TitlePanel"Grid.Row="0"Margin="12,17,0,28"> <TextBlockx:Name="ApplicationTitle"Text="Your Reminder" Style="{StaticResourcePhoneTextNormalStyle}"/> </StackPanel>
<!--ContentPanel - place additional content here--> <Gridx:Name="ContentPanel"Grid.Row="1"Margin="12,0,12,0"> <ListBoxHeight="453"HorizontalAlignment="Left"Name="listBox1" VerticalAlignment="Top"Width="460" /> </Grid> </Grid>
</phone:PhoneApplicationPage> |
ShowReminder.xaml.cs |
|
Alarm
Alarm與Reminder類似,不同之處在於其不允許指定Title屬性及沒有NavigationUri屬性,取而代之的是Sound屬性,Alarm允許應用程式排定一個時間,當時間到時,
OS會顯示出Alarm的視窗並撥放指定的Sound,特別注意該Sound會不停地被重複播放,且聲音會逐漸提高,就像鬧鐘一樣。
圖6
圖7
MainPage.xaml |
<phone:PhoneApplicationPage x:Class="DemoAlarm.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone" xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"d:DesignWidth="480"d:DesignHeight="768" FontFamily="{StaticResourcePhoneFontFamilyNormal}" FontSize="{StaticResourcePhoneFontSizeNormal}" Foreground="{StaticResourcePhoneForegroundBrush}" SupportedOrientations="Portrait"Orientation="Portrait" shell:SystemTray.IsVisible="True" xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"Loaded="PhoneApplicationPage_Loaded">
<!--LayoutRoot is the root grid where all page content is placed--> <Gridx:Name="LayoutRoot"Background="Transparent"> <Grid.RowDefinitions> <RowDefinitionHeight="Auto"/> <RowDefinitionHeight="*"/> </Grid.RowDefinitions>
<!--TitlePanel contains the name of the application and page title--> <StackPanelx:Name="TitlePanel"Grid.Row="0"Margin="12,17,0,28"> <TextBlockx:Name="ApplicationTitle"Text="Alarm Demo" Style="{StaticResourcePhoneTextNormalStyle}"/> </StackPanel>
<!--ContentPanel - place additional content here--> <StackPanelx:Name="ContentPanel"Grid.Row="1"Margin="12,0,12,0"> <TextBlockHorizontalAlignment="Stretch" Name="textBlock1" Text="Begin Time"VerticalAlignment="Top" /> <StackPanelOrientation="Horizontal"> <toolkit:DatePickerx:Name="ctBeginDate" /> <toolkit:TimePickerx:Name="ctBeginTime" /> </StackPanel> <TextBlockHorizontalAlignment="Stretch" Name="textBlock2" Text="Expired Time"VerticalAlignment="Top" /> <StackPanelOrientation="Horizontal"> <toolkit:DatePickerx:Name="ctExpiredDate" /> <toolkit:TimePickerx:Name="ctExpiredTime" /> </StackPanel> <TextBlockHorizontalAlignment="Stretch" Name="textBlock3" Text="Recurrence"VerticalAlignment="Top" /> <toolkit:ListPickerx:Name="ctRecurrence"ListPickerMode="Normal">
</toolkit:ListPicker> <TextBlockHorizontalAlignment="Stretch" Name="textBlock5" Text="Content"VerticalAlignment="Top" /> <TextBoxHeight="71"Name="ctContent"Text=""Width="460" /> <StackPanelOrientation="Horizontal"></StackPanel> <ButtonHorizontalAlignment="Right"Content="Add"Height="71" Name="btnAddOrRemove"Width="160"Click="btnAddOrRemove_Click" /> </StackPanel> </Grid>
</phone:PhoneApplicationPage> |
MainPage.xaml.cs |
|
另外,Content屬性雖然可指定,但Alarm觸發時並不會顯示出來,當使用者點選Alarm提示視窗時,會啟動排入Alarm的應用程式並進入主頁面。
從多工的角度來看,Reminder與Alarm都不算是多工,因為它們的運作方式是透過系統所提供的API,指定特定時間來啟動,用排程的角度來看它們會貼切些,提醒各位,
每個應用程式都可以排入一個以上的Alarm或是Reminder哦。
Scheduled Tasks
相較於Reminder與Alarm,Windows Phone 7.1提供了另一個更趨近於多工的機制: Scheduled Tasks。
Scheduled Tasks可細分為兩類,一是PeriodicTask,當應用程式排入一個PeriodicTask後,OS會不停的循環呼叫這個Task,每個周期約15秒,這有點像是Timer,在這個Task中,我們可以記錄GPS等資訊。
由於Task會不停的被呼叫,此時其執行的工作是否會對裝置電力造成影響就是一個很重要的課題,PeriodicTask屬於執行權較低的Task,OS只允許其使用有限的資源及較低的執行效能,也就是說PeriodicTask
跑起來會比一般應用程式慢就是了。另外,PeriodicTask每30分鐘才會被呼叫一次,而每次被分配到的執行時間是25秒,超過25秒後會有隨時被中斷的可能。
另一個ResourceIntensiveTask則擁有較高的優先權及能使用較多的資源,其被呼叫時間為每10分鐘一次,值得注意的是ResourceIntensiveTask必須要滿足幾個條件才會執行。
必須有外接電源 |
必須擁有Wi-Fi,或是與PC連接(USB) |
電量必須高於90% |
只會在Lock Screen下觸發 |
當電話功能啟動時不會被觸發 |
建立Scheduled Task的方式很簡單,先建立一個Windows Phone 應用程式,接著於同方案中加入一個Scheduled Task Agent專案。
圖8
接著於主專案中新增對此Agent的參考。
圖9
此時Visual Studio 2010會自動在主UI專案的WMAppManifest.xml中加入此Schedule Task Agent的資訊,這是主UI如何載入Agent的關鍵資訊。
WMAppManifest.xml |
?xmlversion="1.0"encoding="utf-8"?> <Deploymentxmlns="http://schemas.microsoft.com/windowsphone/2009/deployment"AppPlatformVersion="7.1"> ……………. <ExtendedTaskName="BackgroundTask"> <BackgroundServiceAgentSpecifier="ScheduledTaskAgent"Name="ScheduledTaskAgent1"Source="ScheduledTaskAgent1"Type="ScheduledTaskAgent1.ScheduledAgent"/> </ExtendedTask> </Tasks> ………. </Deployment> |
於Schedule Task專案中加入以下的程式碼。
ScheduledTaskAgent1\ScheduleTaskAgent1.cs
using System;
using System.Windows;
using Microsoft.Phone.Scheduler;
using Microsoft.Phone.Notification;
using Microsoft.Phone.Shell;
namespace ScheduledTaskAgent1
{
public class ScheduledAgent : ScheduledTaskAgent
{
private static volatile bool _classInitialized;
public ScheduledAgent()
{
if (!_classInitialized)
{
_classInitialized = true;
// Subscribe to the managed exception handler
Deployment.Current.Dispatcher.BeginInvoke(delegate
{
Application.Current.UnhandledException += ScheduledAgent_UnhandledException;
});
}
}
///Code to execute on Unhandled Exceptions
private void ScheduledAgent_UnhandledException(object sender,ApplicationUnhandledExceptionEventArgse)
{
if (System.Diagnostics.Debugger.IsAttached)
{
// An unhandled exception has occurred; break into the debugger
System.Diagnostics.Debugger.Break();
}
}
protected override void OnInvoke(ScheduledTask task)
{
string toastMessage = "";
if (task is PeriodicTask)
toastMessage = "Periodic task running.";
ShellToast toast = new ShellToast();
toast.Title = "Background Agent Sample";
toast.Content = toastMessage;
toast.Show();
#if(DEBUG)
ScheduledActionService.LaunchForTest(task.Name,TimeSpan.FromMinutes(1));
#endif
NotifyComplete();
}
}
}
OnInvoke函式會在此Schedule Task被啟動時呼叫,在實體機器上大約是每30分鐘呼叫一次,如果你想在Emulerator上測試Schedule Task Agent的效果,可以呼叫ScheduledActionService的
LaunchForTest函式,在第二個參數中傳入一個TimeSpan參數,指定此Schedul Task Agent於何時被呼叫(此處指定為1分鐘),注意,LaunchForTest只作用於Emulator,實體機器上還是約30分鐘呼叫一次。
最後於主UI介面加入排入Agent的程式碼。
MainPage.xaml |
<phone:PhoneApplicationPage x:Class="DemoTakeApp.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone" xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"d:DesignWidth="480"d:DesignHeight="768" FontFamily="{StaticResourcePhoneFontFamilyNormal}" FontSize="{StaticResourcePhoneFontSizeNormal}" Foreground="{StaticResourcePhoneForegroundBrush}" SupportedOrientations="Portrait"Orientation="Portrait" shell:SystemTray.IsVisible="True">
<!--LayoutRoot is the root grid where all page content is placed--> <Gridx:Name="LayoutRoot"Background="Transparent"> <StackPanel Orientation="Vertical"Name="PeriodicStackPanel"Margin="0,0,0,40"> <TextBlockText="Periodic Agent"Style="{StaticResourcePhoneTextTitle2Style}"/> <StackPanelOrientation="Horizontal"> <TextBlockText="name: "Style="{StaticResourcePhoneTextAccentStyle}"/> <TextBlockText="{BindingName}" /> </StackPanel> <StackPanelOrientation="Horizontal"> <TextBlockText="is enabled"VerticalAlignment="Center" Style="{StaticResourcePhoneTextAccentStyle}"/> <CheckBoxName="PeriodicCheckBox"IsChecked="{BindingIsEnabled}"Checked="PeriodicCheckBox_Checked"Unchecked="PeriodicCheckBox_Unchecked"/> </StackPanel> <StackPanelOrientation="Horizontal"> <TextBlockText="is scheduled: " Style="{StaticResourcePhoneTextAccentStyle}"/> <TextBlockText="{BindingIsScheduled}" /> </StackPanel> <StackPanelOrientation="Horizontal"> <TextBlockText="last scheduled time: " Style="{StaticResourcePhoneTextAccentStyle}"/> <TextBlockText="{BindingLastScheduledTime}" /> </StackPanel> <StackPanelOrientation="Horizontal"> <TextBlockText="expiration time: "Style="{StaticResourcePhoneTextAccentStyle}"/> <TextBlockText="{BindingExpirationTime}" /> </StackPanel> <StackPanelOrientation="Horizontal"> <TextBlockText="last exit reason: " Style="{StaticResourcePhoneTextAccentStyle}"/> <TextBlockText="{BindingLastExitReason}" /> </StackPanel> </StackPanel> </Grid> </phone:PhoneApplicationPage> |
MainPage.xaml.cs |
|
UI部分只是顯示Schedule Task Agent的資訊,不需做太多解釋,當使用者勾選畫面上的CheckBox時,StartPeriodicAgent函式會被呼叫,此處以下列的程式碼來來取得先前排入的Periodic Agent:
periodicTask = ScheduledActionService.Find(periodicTaskName) as PeriodicTask; |
當然,首次呼叫時這裡的回傳值一定是Null,這時開始建立PeriodicTask物件,並呼叫ScheduleActionService.Add來排入Schedule Task Agent。
periodicTask = new PeriodicTask(periodicTaskName); ScheduledActionService.Add(periodicTask); |
這裡你應該會開始有點困惑,我們建立了一個PeriodicTask物件,接著將他排入Schedule,但問題是,這個PeriodicTask與我們所建立的ScheduleTaskAgent1怎麼扯上關係的?
是透過以下的WMAppManifest.xml嗎?
<BackgroundServiceAgentSpecifier="ScheduledTaskAgent"Name="ScheduledTaskAgent1"Source="ScheduledTaskAgent1"Type="ScheduledTaskAgent1.ScheduledAgent"/> |
但問題是,我們建立PeriodicTask物件時所傳入的Name(PeriodicTask),並不等於WMAppManifest.xml中定義的那個Name(ScheduleTaskAgent1),這兩個到底是怎麼搭起來的?這有個關鍵,
每個Windows Phone Application都只能夠擁有一個Scheduled Task Agent,這意味著同一個Windows Phone Application中無法建立兩個Scheduled Task Agent,所以,當建立PeriodicTask並
呼叫ScheduleActionService時,其會依據WMAppManifest.xml中的資訊將PeriodicTask物件與描述的ScheduledAgent連結起來,而建立PeriodicTask時,指定的Name則只是一個戳記,當
Schedule Agent排入Schedule後,我們就只能夠透過ScheduledActionService.Find函式加上PeriodicTask的Name來取得該Schedule Agent物件,進而改變或顯示其資訊。
本例執行結果如下圖:
圖10
按下Back鍵離開程式,此時應能於Backgound Tasks中看到如圖11的資訊。
圖11
等待約幾分鐘後,就可見到ShellToast顯示。
圖12
提醒各位,在實機上PeriodicTask 30分鐘才會被呼叫一次,另外,要注意不要呼叫不被允許的APIs,否則會無法通過審核。
不被允許在Scheduled Task中使用的APIs http://msdn.microsoft.com/en-us/library/hh202962(v=vs.92).aspx |
ResourceIntensiveTask的寫法與PeriodicTask幾近相同,差別在於Task物件的建立,剩下的用法都一樣。
resourceIntensiveTask = new ResourceIntensiveTask(resourceIntensiveTaskName); resourceIntensiveTask.Description = "This demonstrates a resource-intensive task."; ……… ScheduledActionService.Add(resourceIntensiveTask); |
Background Audio Agent
在Windows Phone 7.0中,應用程式可透過三種方式來撥放音樂,一是使用MediaPlayer物件,這種方式即使在應用程式被關閉後也能持續撥放音樂,缺點是每次只能撥放一首音樂,應用程式必須
自己控制才能連續撥放多首音樂(也就是說透過事件處理,在一首音樂結束後,緊接著再呼叫MediaPlayer來撥放另一首),但倘若應用程式被關閉了,那麼當播完結束前的那首音樂後,就不會再持續撥放了
(因為應用程式已經不在了,所以自然無法在音樂結束後再撥放另一首)。
第二種是透過XNA的SoundEffect來撥放音樂,這種方式與MediaPlayer截然不同,因為當應用程式結束後,音樂也會跟著結束,不管是否已撥到尾端。
第三種是透過MediaElement,這個方式與SoundEffect一樣,當應用程式結束後,音樂也會跟著結束,不管是否已撥到尾端。
影響音樂撥放的還有Lockscreen的問題,當進入Lockscreen後,用SoundEffect,MediaElement撥放的音樂都會被終止掉,除非應用程式設定為Run Under Lockscreen。
這三種方式對於單純只是撥放音樂的應用程式而言確實已足夠,但對於需要持續撥放音樂的應用程式,例如音樂撥放器來說,其實是不足的。因此,Windows Phone 7.1中提供了Background Audid Agent機制,
讓類似音樂撥放器的應用程式可以進行背景音樂的播放工作,Background Audid Agent 一方面可以讓應用程式播放音樂,另一方面還能在應用程式關閉後依舊持續撥放音樂,且不限單首。
建立Background Audio Agent與建立Scheduled Agent的過程大致相同,先建立UI應用程式,再加入適當的Agent專案,接著讓UI應用程式專案參考該Agent專案,差別在於選擇建立Agent的專案類型,Background Audio Agent
提供了兩種專案樣板,一是Audio Playback Agent,可以撥放本地端(位於Isolated Storage)及網路上的的音樂檔案,另一個是Audio Streaming Agent,這是用來輔助Audio Playback Agent的,可以讓開發人員撥放Windows Phone 7
所不支援的音樂格式,也可以進行較為複雜的Streaming(串流)式撥放,本文先就Audio Playback Agent做介紹,首先建立UI應用程式,接著加入新專案,選擇Audio Playback Agent。
圖13
完成後開始設計UI介面,如下所示:
MainPage.xaml |
<phone:PhoneApplicationPage x:Class="DemoBackgroundAudio.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone" xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"d:DesignWidth="480"d:DesignHeight="768" FontFamily="{StaticResourcePhoneFontFamilyNormal}" FontSize="{StaticResourcePhoneFontSizeNormal}" Foreground="{StaticResourcePhoneForegroundBrush}" SupportedOrientations="Portrait"Orientation="Portrait" shell:SystemTray.IsVisible="True">
<!--LayoutRoot is the root grid where all page content is placed--> <Gridx:Name="LayoutRoot"Background="Transparent"> <Grid.RowDefinitions> <RowDefinitionHeight="Auto"/> <RowDefinitionHeight="*"/> </Grid.RowDefinitions>
<!--TitlePanel contains the name of the application and page title--> <StackPanelx:Name="TitlePanel"Grid.Row="0"Margin="12,17,0,28"> <TextBlockx:Name="ApplicationTitle"Text="MY APPLICATION"Style="{StaticResourcePhoneTextNormalStyle}"/> </StackPanel>
<!--ContentPanel - place additional content here--> <Gridx:Name="ContentPanel"Grid.Row="1"Margin="12,0,12,0"> <ButtonContent="Play"Height="72"HorizontalAlignment="Left"Margin="141,45,0,0"Name="button1"VerticalAlignment="Top"Width="160"Click="button1_Click" /> <ButtonContent="Next"Height="72"HorizontalAlignment="Left"Margin="141,136,0,0"Name="button2"VerticalAlignment="Top"Width="160"Click="button2_Click" /> <ButtonContent="Prev"Height="72"HorizontalAlignment="Left"Margin="141,227,0,0"Name="button3"VerticalAlignment="Top"Width="160"Click="button3_Click" /> <ButtonContent="Reset"Height="72"HorizontalAlignment="Left"Margin="141,316,0,0"Name="button4"VerticalAlignment="Top"Width="160"Click="button4_Click" /> <SliderHeight="149"HorizontalAlignment="Left"Margin="6,515,0,0"Name="slider1"VerticalAlignment="Top"Width="460"Maximum="1"LargeChange="0.5"ValueChanged="slider1_ValueChanged" /> <TextBlockHeight="29"HorizontalAlignment="Left"Margin="12,480,0,0"Name="textBlock1"Text="Volume"VerticalAlignment="Top" /> <TextBlockHeight="30"HorizontalAlignment="Left"Margin="12,394,0,0"Name="textBlock2"Text=""VerticalAlignment="Top"Width="373" /> </Grid> </Grid> </phone:PhoneApplicationPage> |
MainPage.xaml.cs |
|
畫面上總共有4個Button,分別用來撥放、下一首、上一首及Reset(重新從第一首開始撥放),兩個TextBlock,一個顯示目前歌曲,另一個則是”Volume”的標記,底下是一個Slider,用來控制音量。
程式一開始,就把位於專案中目錄下的5個MP3複製到Isolated Storage中,因為BackgroundAudioPlayer只能撥放Isolated Storage中的音樂檔案。
publicMainPage() { InitializeComponent(); CopyToIsolatedStorage(); …………. } |
這些檔案本例是放在專案中,以Content型態。
圖15
圖16
接著將音量反映至Slider上,WP7的App審核標準中,所有撥放音效的應用程式,都必須提供音量調整的機制。
slider1.Value = BackgroundAudioPlayer.Instance.Volume; |
緊接著,掛載事件至BackgroundAudioPlayer的StateChanged,用來反映目前撥放的音樂名稱等等資訊。
|
由於Background Audio Agent不受UI程式是否在執行中的影響,當UI程式被關閉在啟動時,自然得反映目前正在撥放的曲目。
|
當使用者按下Play按鈕後,會開始撥放音樂。
|
這裡有個重點,因為Background Audio Agent與UI應用程式是完全不同的兩個程式,彼此間需要依賴Isolated Storage來溝通,此處透過CreateTrackList函式,將要撥放的音樂列表存成一個Track.lst檔案,
稍後Background Audio Agent將依據此檔案的內容來撥放音樂檔案。
|
CreateResetCmd函式的用途很簡單,只是在Isolated Storage放入一個reset.cmd檔案,當Background Audio Agent發現這個檔案的存在後,會直接重新由Track.lst取得音樂曲目,從第一首開始撥放。
剩下的四個函式就只是反映UI的動作,例如Next、Prev、Reset。
|
到此,UI端已經完成,接著修改AudioPlaybackAgent1專案內容。
AudioPlayer1.cs |
|
當UI程式對BackgroundAudioPlayer.Instance下達任何函式呼叫時,會觸發OnUserAction函式,此處我們只針對Play狀態做修改,其它都維持原樣。
|
這裡可以看到NeedReset函式的作用,當UI程式在Isolated Storage放入reset.cmd後,NeedReset會回傳True,此時從Isolated Storage中的Track.lst讀取要撥放的曲目,然後進行撥放。GetNextTrack、GetPreviousTrack則是
在上一首、下一首動作時被呼叫,單純只是控制游標而已,值得一提的是,當player.Track是null時,其呼叫的是GetPreviousTrack ,這通常發生在首次按下Play鍵時,也就是第一次撥放。如果需要撥放的是位於遠端網站上的
音樂檔案的話,可於建立AudioTrack物件時傳入檔案的網址,如下所示:
|
圖17
Windows Phone Marketplace對於撥放音樂的App,有著幾個審核重點,如果違反其中一條就會被退件。
- 必須尊重Zune,如果使用者已經使用Zune撥放音樂,那麼App在停掉Zune音樂前得先提醒使用者。
- 必須提供音量調整機制
Audio Streaming Agent
在很多情況下,我們希望可以在音樂檔案下載同時進行音樂的撥放動作,也就是Streaming(串流),雖然原本的Audio Playback Agent已具備Streaming的基本能力,但如果遭遇到來源網站需要先行認證,例如在
HTTP Request Header放上認證資訊時,此時Audio Playback Agent就幫不上忙了,另一種情況則是需要撥放Windows Phone 7所不支援的音樂格式。
要解決這兩種情況,必須結合兩種技術:Audio Streaming Agent及MediaStreamSource,前者與Audio Playback Agent結合後,可以讓我們在音樂撥放前才提供來源的Streaming,
但這個Stream型別必須要是繼承自MediaStreamSource,對於不熟悉MediaStreamSource結構的開發人員而言,要實做這個東西確實有些困難,還好網路上已經有一個Open Source的專案,提供了MP3StreamSource,
這與 內建的MP3撥放能力不同,其擁有了直接從NetworkStream解碼成可撥放音樂格式,並變成MediaStreamSource的能力,結合這兩者後,可以在Audio Streaming Agent中以WebClient像伺服器要求認證,完成後透過OpenReadAsyn,
再透過MP3SteramSource來轉換NetworkStream,一方面解決認證的問題,另一方面也達到了串流撥放的目的,該專案網址如下:
本文將透過此Library,實做一個可透過WebClient下載,並經由NetworkStream直接進行Streaming Play的範例,此處將重用之前Audio Playback Agent範例中UI介面、Audio Playback Agent的大部分程式碼,如下所示:
MainPage.xaml.cs |
|
AudioPlaybackAgent1.cs |
|
特別注意一點,UI部分(MainPage.xaml.cs)已經將自Resource複製MP3至Isolated Storage中的程式碼,因為此例將透過Audio Streaming Agent自網站下載MP3,而AudioPlaybackAgent1.cs中建立AudioTrack的部分也已經將Uri部分以Null傳入,
這代表著其將會觸發Audio Stream Agent中的OnBeginStreaming函式,這裡我們將透過網站下載.MP3。
接著,請建立一個新專案,選擇Audio Streaming Agent類型。
圖18
然後要引用MP3StreamSource所在的Projects,整個專案結構如下:
圖19
以下為AudioStreamAgent1.cs的程式碼。
AudioStreamAgent1.cs |
using System; using Microsoft.Phone.BackgroundAudio; using System.Net;
namespace AudioStreamAgent1 { public class AudioTrackStreamer : AudioStreamingAgent {
protected override void OnBeginStreaming(AudioTrack track, AudioStreamer streamer) { //TODO: Set the SetSource property of streamer to a MSS source WebClient client = new WebClient(); client.OpenReadCompleted += (s,args)=> { streamer.SetSource(new Media.Mp3MediaStreamSource(args.Result)); NotifyComplete(); }; client.OpenReadAsync(new Uri("http://localhost:62048/"+track.Title,UriKind.Absolute),streamer); } protected override void OnCancel() { base.OnCancel(); } } } |
我們的MP3是放在http://localhost:62048這個Web Application下,這裡透過WebClient來下載,然後得到NetworkStream,最後交給MP3MediaStreamSource,接著交給streamer來撥放,
記得!NotifyComplete必須在完成所有動作後呼叫,這個函式是告知Windows Phone 7 Runtime,此Agent的任務已經完成。