[WP7]Windows Phone 7.1 Overview (2) – Background Agents

雖然新的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


using System;

using System.Collections.Generic;

using System.Linq;

using System.Net;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Documents;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Animation;

using System.Windows.Shapes;

using Microsoft.Phone.Controls;

using Microsoft.Phone.Scheduler;
 

namespace DemoReminder

{

    public partial class MainPage : PhoneApplicationPage

    {

        // Constructor

        public MainPage()

        {

            InitializeComponent();

            List recurrenceItems = new List()

            {

                "None",

                "Daily",

                "Weekly",

                "Monthly",

                "EndOfMonth",

                "Yearly"

            };

            ctRecurrence.ItemsSource = recurrenceItems;

        }


        private void btnAddOrRemove_Click(object sender, RoutedEventArgs e)

        {

            var myReminder = ScheduledActionService.GetActions().Where(a => a.Name == "myReminder").FirstOrDefault();

            if (myReminder != null)

            {

                ScheduledActionService.Remove(myReminder.Name);

                btnAddOrRemove.Content = "Add";

            }

            else

            {

                if (ctBeginDate.Value> ctExpiredDate.Value)

                {

                    MessageBox.Show("expired date must greater begin time.");

                    return;

                }

                myReminder = new Reminder("myReminder");

                myReminder.BeginTime = ctBeginDate.Value.Value +ctBeginTime.Value.Value.TimeOfDay;

                myReminder.ExpirationTime = ctExpiredDate.Value.Value + ctExpiredTime.Value.Value.TimeOfDay;

                myReminder.Title = ctTitle.Text;

                myReminder.Content = ctContent.Text;

                myReminder.RecurrenceType = (RecurrenceInterval)Enum.Parse(typeof(RecurrenceInterval),  (string)ctRecurrence.SelectedItem, true);

                myReminder.NavigationUri = newUri(string.Format("/ShowReminder.xaml?Param1={0}&Param2={1}",  ctParam1.Text, ctParam2.Text), UriKind.Relative);

                ScheduledActionService.Add(myReminder);

                btnAddOrRemove.Content = "Remove";

            }

        }


        private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)

        {

            var myReminder = ScheduledActionService.GetActions().Where( a => a.Name == "myReminder").FirstOrDefault();

            if (myReminder != null)

            {

                ctTitle.Text = myReminder.Title;

                ctContent.Text = myReminder.Content;

                ctBeginDate.Value = myReminder.BeginTime.Date;

                ctBeginTime.Value = myReminder.BeginTime;

                ctExpiredDate.Value = myReminder.ExpirationTime.Date;

                ctExpiredTime.Value = myReminder.ExpirationTime;

                ctRecurrence.SelectedItem = myReminder.RecurrenceType.ToString();

                string uri = myReminder.NavigationUri.OriginalString;

                ctParam1.Text = uri.Substring(uri.IndexOf("Param1=") + 7,  uri.IndexOf("&") - (uri.IndexOf("Param1=") + 7));

                ctParam2.Text = uri.Substring(uri.IndexOf("Param2=") + 7);

                btnAddOrRemove.Content = "Remove";

            }

            else

                btnAddOrRemove.Content = "Add";

        }

    }

}

 

  

  讓我稍微解釋一下這個範例,當使用者設定了各個參數按下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


using System;

using System.Collections.Generic;

using System.Linq;

using System.Net;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Documents;
 
using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Animation;

using System.Windows.Shapes;

using Microsoft.Phone.Controls;

using Microsoft.Phone.Scheduler;


namespace DemoReminder

{

    public partial class ShowReminder : PhoneApplicationPage

    {

        public ShowReminder()

        {

            InitializeComponent();

        }


        protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)

        {

            Reminder myReminder = ScheduledActionService.GetActions().Where(  a => a.Name == "myReminder").FirstOrDefault();

            if (myReminder != null)

            {

                listBox1.Items.Add("Being DateTime:" + myReminder.BeginTime.ToString());

                listBox1.Items.Add("Expired DateTime:" +  myReminder.ExpirationTime.ToString());

                listBox1.Items.Add("Title:" + myReminder.Title);

                listBox1.Items.Add("Content:" + myReminder.Content);

                listBox1.Items.Add("Recurrence:" + myReminder.RecurrenceType.ToString());

                listBox1.Items.Add("-----------Params:----------");               

            }

            foreach (var item in NavigationContext.QueryString.Keys)

                listBox1.Items.Add("Param:" + item + ", Value:" +  NavigationContext.QueryString[item]);

            base.OnNavigatedTo(e);

        }

        private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)

        {           

        }

    }

}

 

 

 

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


using System;

using System.Collections.Generic;

using System.Linq;

using System.Net;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Documents;

using System.Windows.Input;

using System.Windows.Media;
 
using System.Windows.Media.Animation;

using System.Windows.Shapes;

using Microsoft.Phone.Controls;

using Microsoft.Phone.Scheduler;


namespace DemoAlarm

{

    public partial class MainPage : PhoneApplicationPage

    {

        // Constructor

        public MainPage()

        {

            InitializeComponent();

            List recurrenceItems = new List()

            {

                "None",

                "Daily",

                "Weekly",

                "Monthly",

                "EndOfMonth",

                "Yearly"

            };

            ctRecurrence.ItemsSource = recurrenceItems;

        }


        private void btnAddOrRemove_Click(object sender, RoutedEventArgs e)

        {

            var myAlarm = ScheduledActionService.GetActions().Where( a => a.Name == "myAlarm").FirstOrDefault();

            if (myAlarm != null)

            {

                ScheduledActionService.Remove(myAlarm.Name);

                btnAddOrRemove.Content = "Add";

            }

            else

            {

                if (ctBeginDate.Value> ctExpiredDate.Value)

                {

                    MessageBox.Show("expired date must greater begin time.");

                    return;

                }

                myAlarm = new Alarm("myAlarm");

                myAlarm.BeginTime = ctBeginDate.Value.Value + ctBeginTime.Value.Value.TimeOfDay;

                myAlarm.ExpirationTime = ctExpiredDate.Value.Value +  ctExpiredTime.Value.Value.TimeOfDay;

                myAlarm.RecurrenceType = (RecurrenceInterval)Enum.Parse(  typeof(RecurrenceInterval),      (string)ctRecurrence.SelectedItem, true);

                myAlarm.Sound = new Uri("/0088.wav", UriKind.Relative);

                ScheduledActionService.Add(myAlarm);

                btnAddOrRemove.Content = "Remove";

            }

        }


        private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)

        {

            var myAlarm = ScheduledActionService.GetActions().Where(  a => a.Name == "myAlarm").FirstOrDefault();

            if (myAlarm != null)

            {

                ctContent.Text = myAlarm.Content;

                ctBeginDate.Value = myAlarm.BeginTime.Date;

                ctBeginTime.Value = myAlarm.BeginTime;

                ctExpiredDate.Value = myAlarm.ExpirationTime.Date;

                ctExpiredTime.Value = myAlarm.ExpirationTime;

                ctRecurrence.SelectedItem = myAlarm.RecurrenceType.ToString();

                btnAddOrRemove.Content = "Remove";

            }

            else

                btnAddOrRemove.Content = "Add";

        }

    }

}

 

 

 

另外,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


using System;

using System.Collections.Generic;

using System.Linq;

using System.Net;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Documents;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Animation;

using System.Windows.Shapes;

using Microsoft.Phone.Controls;

using Microsoft.Phone.Scheduler;


namespace DemoTakeApp

{

    public partial class MainPage : PhoneApplicationPage

    {

        PeriodicTask periodicTask;


        string periodicTaskName = "PeriodicAgent";

        public bool agentsAreEnabled = true;


        // Constructor

        public MainPage()

        {

            InitializeComponent();

        }


        private void StartPeriodicAgent()

        {

            agentsAreEnabled = true;


            periodicTask = ScheduledActionService.Find(periodicTaskName) as PeriodicTask;


            // If the task already exists and background agents are enabled for the

            // application, you must remove the task and then add it again to update

            // the schedule

            if (periodicTask != null)

                RemoveAgent(periodicTaskName);


            periodicTask = new PeriodicTask(periodicTaskName);


            // The description is required for periodic agents. This is the string that the user

            // will see in the background services Settings page on the device.

            periodicTask.Description = "This demonstrates a periodic task.";


            // Place the call to Add in a try block in case the user has disabled agents.

            try

            {

                ScheduledActionService.Add(periodicTask);

                PeriodicStackPanel.DataContext = periodicTask;

#if(DEBUG)

                ScheduledActionService.LaunchForTest(periodicTaskName, TimeSpan.FromMinutes(1));

#endif

            }

            catch (InvalidOperationException exception)

            {

                if (exception.Message.Contains("BNS Error: The action is disabled"))

                {

                    MessageBox.Show(

"Background agents for this application have been disabled by the user.");

                    agentsAreEnabled = false;

                    PeriodicCheckBox.IsChecked = false;

                }

            }

        }


        private void RemoveAgent(string name)

        {

            try

            {

                ScheduledActionService.Remove(name);

            }

            catch (Exception)

            {

            }

        }


        private void PeriodicCheckBox_Checked(object sender, RoutedEventArgs e)

        {

            StartPeriodicAgent();

        }


        private void PeriodicCheckBox_Unchecked(object sender, RoutedEventArgs e)

        {

            RemoveAgent(periodicTaskName);

        }

    }

}

 

 

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


using System;

using System.Collections.Generic;

using System.Linq;

using System.Net;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Documents;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Animation;

using System.Windows.Shapes;

using Microsoft.Phone.Controls;

using System.Windows.Resources;

using System.IO;

using System.IO.IsolatedStorage;

using Microsoft.Phone.BackgroundAudio;

using Microsoft.Xna.Framework.Media;


namespace DemoBackgroundAudio

{

    public partial class MainPage : PhoneApplicationPage

    {

        private void CopyToIsolatedStorage()

        {

            using (IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForApplication())

            {

                string[] files = new string[] { "01.mp3", "02.mp3", "03.mp3", "04.mp3", "05.mp3" };


                foreach (var _fileName in files)

                {

                    if (!storage.FileExists(_fileName))

                    {

                        string _filePath = "Musics/" + _fileName;

                        StreamResourceInfo resource = Application.GetResourceStream(new Uri(_filePath, UriKind.Relative));


                        using (IsolatedStorageFileStream file = storage.CreateFile(_fileName))

                        {

                            int chunkSize = 4096;

                            byte[] bytes = new byte[chunkSize];

                            int byteCount;


                            while ((byteCount = resource.Stream.Read(bytes, 0, chunkSize)) > 0)

                                file.Write(bytes, 0, byteCount);

                        }

                    }

                }

            }

        }


        private void CreateTrackList(string[] tracks)

        {           

            using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())

            {

                using (IsolatedStorageFileStream fs = new IsolatedStorageFileStream("track.lst", FileMode.Create, FileAccess.Write, isf))

                {

                    using (StreamWriter sw = new StreamWriter(fs))

                    {

                        foreach (var track in tracks)

                            sw.WriteLine(track);

                    }

                }

            }

        }


        private void CreateResetCmd()

        {

            using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())

            {

                using (IsolatedStorageFileStream fs = new IsolatedStorageFileStream("reset.cmd", FileMode.Create, FileAccess.Write, isf))

                {

                    using (StreamWriter sw = new StreamWriter(fs))

                    {

                        sw.WriteLine("reset");

                    }

                }

            }

        }


        // Constructor

        public MainPage()

        {

            InitializeComponent();

            CopyToIsolatedStorage();

            slider1.Value = BackgroundAudioPlayer.Instance.Volume;

            BackgroundAudioPlayer.Instance.PlayStateChanged += new EventHandler(Instance_PlayStateChanged);

            if (BackgroundAudioPlayer.Instance.PlayerState == PlayState.Playing)

                textBlock1.Text = BackgroundAudioPlayer.Instance.Track.Title;

        }


        void Instance_PlayStateChanged(object sender, EventArgs e)

        {

            if (BackgroundAudioPlayer.Instance.PlayerState == PlayState.Playing)

                textBlock2.Text = BackgroundAudioPlayer.Instance.Track.Title;

        }


        private void button1_Click(object sender, RoutedEventArgs e)

        {

            if (BackgroundAudioPlayer.Instance.PlayerState == PlayState.Stopped || BackgroundAudioPlayer.Instance.PlayerState == PlayState.Unknown)

            {

                string[] files = new string[] { "01.mp3", "02.mp3", "03.mp3", "04.mp3", "05.mp3" };

                CreateTrackList(files);

                CreateResetCmd();               

            }

            if (BackgroundAudioPlayer.Instance.PlayerState != PlayState.Playing)

            {

                if (MediaPlayer.State == MediaState.Paused || MediaPlayer.State == MediaState.Playing)

                {

                    if (MessageBox.Show("media player is playing or paused, are you want stop it and play current choosed?", "video player", M
                                                                        essageBoxButton.OKCancel) == MessageBoxResult.Cancel)

                        return;

                }

                BackgroundAudioPlayer.Instance.Play();

            }


        }


        private void button2_Click(object sender, RoutedEventArgs e)

        {

            BackgroundAudioPlayer.Instance.SkipNext();

        }


        private void button3_Click(object sender, RoutedEventArgs e)

        {

            BackgroundAudioPlayer.Instance.SkipPrevious();

        }


        private void button4_Click(object sender, RoutedEventArgs e)

        {

            BackgroundAudioPlayer.Instance.Stop();           

            CreateResetCmd();

            BackgroundAudioPlayer.Instance.Play();

        }


        private void slider1_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)

        {

            BackgroundAudioPlayer.Instance.Volume = slider1.Value;

        }

    }

}

畫面上總共有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,用來反映目前撥放的音樂名稱等等資訊。

 


BackgroundAudioPlayer.Instance.PlayStateChanged += new EventHandler(Instance_PlayStateChanged);

……………………………..

voidInstance_PlayStateChanged(object sender, EventArgs e)

{

            if (BackgroundAudioPlayer.Instance.PlayerState == PlayState.Playing)

                textBlock2.Text = BackgroundAudioPlayer.Instance.Track.Title;

}

由於Background Audio Agent不受UI程式是否在執行中的影響,當UI程式被關閉在啟動時,自然得反映目前正在撥放的曲目。

 


if(BackgroundAudioPlayer.Instance.PlayerState == PlayState.Playing)

   textBlock1.Text = BackgroundAudioPlayer.Instance.Track.Title;

當使用者按下Play按鈕後,會開始撥放音樂。

 


private void button1_Click(object sender, RoutedEventArgs e)

{

       if (BackgroundAudioPlayer.Instance.PlayerState == PlayState.Stopped ||

                        BackgroundAudioPlayer.Instance.PlayerState == PlayState.Unknown)

            {

                string[] files = new string[] { "01.mp3", "02.mp3", "03.mp3", "04.mp3", "05.mp3" };

                CreateTrackList(files);

                CreateResetCmd();               

            }

            if (BackgroundAudioPlayer.Instance.PlayerState != PlayState.Playing)

            {

                if (MediaPlayer.State == MediaState.Paused || MediaPlayer.State == MediaState.Playing)

                {

                    if (MessageBox.Show("media player is playing or paused, are you want stop it and play current choosed?", "
                                            video player", MessageBoxButton.OKCancel) == MessageBoxResult.Cancel)

                        return;

                }

                BackgroundAudioPlayer.Instance.Play();

            }

}

這裡有個重點,因為Background Audio Agent與UI應用程式是完全不同的兩個程式,彼此間需要依賴Isolated Storage來溝通,此處透過CreateTrackList函式,將要撥放的音樂列表存成一個Track.lst檔案,

稍後Background Audio Agent將依據此檔案的內容來撥放音樂檔案。

 


private void CreateTrackList(string[] tracks)

{           

            using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())

            {

                using (IsolatedStorageFileStream fs = new IsolatedStorageFileStream("track.lst", FileMode.Create, FileAccess.Write, isf))

                {

                    using (StreamWriter sw = new StreamWriter(fs))

                    {

                        foreach (var track in tracks)

                            sw.WriteLine(track);

                    }

                }

            }

}

CreateResetCmd函式的用途很簡單,只是在Isolated Storage放入一個reset.cmd檔案,當Background Audio Agent發現這個檔案的存在後,會直接重新由Track.lst取得音樂曲目,從第一首開始撥放。

剩下的四個函式就只是反映UI的動作,例如Next、Prev、Reset。

 


private void button2_Click(object sender, RoutedEventArgs e)

{

            BackgroundAudioPlayer.Instance.SkipNext();

}


privatevoid button3_Click(object sender, RoutedEventArgs e)

{

            BackgroundAudioPlayer.Instance.SkipPrevious();

}


privatevoid button4_Click(object sender, RoutedEventArgs e)

{

            BackgroundAudioPlayer.Instance.Stop();           

            CreateResetCmd();

            BackgroundAudioPlayer.Instance.Play();

}


privatevoid slider1_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)

{

            BackgroundAudioPlayer.Instance.Volume = slider1.Value;

}

到此,UI端已經完成,接著修改AudioPlaybackAgent1專案內容。

 

AudioPlayer1.cs


using System;

using System.Collections;

using System.Collections.Generic;

using System.Windows;

using Microsoft.Phone.BackgroundAudio;

using System.Windows.Resources;

using System.IO;

using System.IO.IsolatedStorage;


namespace AudioPlaybackAgent1

{

    public class AudioPlayer : AudioPlayerAgent

    {

        private static volatile bool _classInitialized;

        private static List _tracks = new List();

        private static int _currentTrack = -1;


        public AudioPlayer()

        {

            if (!_classInitialized)

            {

                _classInitialized = true;

                // Subscribe to the managed exception handler

                Deployment.Current.Dispatcher.BeginInvoke(delegate

                {

                    Application.Current.UnhandledException += AudioPlayer_UnhandledException;

                });

            }

        }


        ///Code to execute on Unhandled Exceptions

        private void AudioPlayer_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e)

        {

            if (System.Diagnostics.Debugger.IsAttached)

            {

                // An unhandled exception has occurred; break into the debugger

                System.Diagnostics.Debugger.Break();

            }

        }


        private void LoadTrack()

        {

            using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())

            {

                using (IsolatedStorageFileStream fs = new IsolatedStorageFileStream("track.lst", FileMode.Open, FileAccess.Read, isf))

                {

                    _tracks.Clear();

                    using (StreamReader sr = new StreamReader(fs))

                    {

                        while (sr.Peek() != -1)

                            _tracks.Add(sr.ReadLine());

                    }

                }

            }

            _currentTrack = -1;

        }


        private bool NeedReset()

        {

            using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())

            {

                if (isf.FileExists("reset.cmd"))

                {

                    isf.DeleteFile("reset.cmd");

                    return true;

                }

            }

            return false;

        }

      


        protected override void OnPlayStateChanged(BackgroundAudioPlayer player, AudioTrack track, PlayState playState)

        {

            switch (playState)

            {

                case PlayState.TrackEnded:

                    player.Track = GetPreviousTrack();

                    break;

                case PlayState.TrackReady:

                    player.Play();

                    break;

                case PlayState.Shutdown:

                    // TODO: Handle the shutdown state here (e.g. save state)

                    break;

                case PlayState.Unknown:

                    break;

                case PlayState.Stopped:

                    break;

                case PlayState.Paused:

                    break;

                case PlayState.Playing:

                    break;

                case PlayState.BufferingStarted:

                    break;

                case PlayState.BufferingStopped:

                    break;

                case PlayState.Rewinding:

                    break;

                case PlayState.FastForwarding:

                    break;

            }


            NotifyComplete();

        }


        protected override void OnUserAction(BackgroundAudioPlayer player, AudioTrack track, UserAction action, object param)

        {

            switch (action)

            {

                case UserAction.Play:

                    if (NeedReset())

                    {

                        LoadTrack();

                        if (player.Track != null) //not reset

                            player.Track = null;

                    }

                    if(player.PlayerState != PlayState.Playing)

                        player.Play();

                    break;

                case UserAction.Stop:

                    player.Stop();

                    break;

                case UserAction.Pause:

                    player.Pause();

                    break;

                case UserAction.FastForward:

                    player.FastForward();

                    break;

                case UserAction.Rewind:

                    player.Rewind();

                    break;

                case UserAction.Seek:

                    player.Position = (TimeSpan)param;

                    break;

                case UserAction.SkipNext:

                    player.Track = GetNextTrack();

                    break;

                case UserAction.SkipPrevious:

                    AudioTrack previousTrack = GetPreviousTrack();

                    if (previousTrack != null)

                    {

                        player.Track = previousTrack;

                    }

                    break;

            }


            NotifyComplete();

        }



       private AudioTrack GetNextTrack()

        {

            // TODO: add logic to get the next audio track

            if(_currentTrack < _tracks.Count - 1)

                _currentTrack++;

            else

                _currentTrack = 0;


            AudioTrack track = new AudioTrack(new Uri(_tracks[_currentTrack],UriKind.Relative),_tracks[_currentTrack],"","",null);


            // specify the track


            return track;

        }


        private AudioTrack GetPreviousTrack()

        {

            // TODO: add logic to get the previous audio track


            if (_currentTrack > 0)

                _currentTrack--;

            else

                _currentTrack = 0 ;


            AudioTrack track = new AudioTrack(new Uri(_tracks[_currentTrack], UriKind.Relative), _tracks[_currentTrack], "", "", null);


            return track;

        }


      

        protected override void OnError(BackgroundAudioPlayer player, AudioTrack track, Exception error, bool isFatal)

        {

            if (isFatal)

            {

                Abort();

            }

            else

            {

                NotifyComplete();

            }


        }

   

        protected override void OnCancel()

        {


        }

    }

}

當UI程式對BackgroundAudioPlayer.Instance下達任何函式呼叫時,會觸發OnUserAction函式,此處我們只針對Play狀態做修改,其它都維持原樣。

 


case UserAction.Play:

    if (NeedReset())

    {

          LoadTrack();

          if (player.Track != null) //not reset

            player.Track = null;

    }

    if(player.PlayerState != PlayState.Playing)

    player.Play();

    break;

這裡可以看到NeedReset函式的作用,當UI程式在Isolated Storage放入reset.cmd後,NeedReset會回傳True,此時從Isolated Storage中的Track.lst讀取要撥放的曲目,然後進行撥放。GetNextTrack、GetPreviousTrack則是

在上一首、下一首動作時被呼叫,單純只是控制游標而已,值得一提的是,當player.Track是null時,其呼叫的是GetPreviousTrack ,這通常發生在首次按下Play鍵時,也就是第一次撥放。如果需要撥放的是位於遠端網站上的

音樂檔案的話,可於建立AudioTrack物件時傳入檔案的網址,如下所示:

 


new AudioTrack(newUri("http://traffic.libsyn.com/wpradio/WPRadio_29.mp3", UriKind.Absolute),

                    "Episode 29",

                    "Windows Phone Radio",

                    "Windows Phone Radio Podcast",

                   null)

 

圖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,一方面解決認證的問題,另一方面也達到了串流撥放的目的,該專案網址如下:

 

http://archive.msdn.microsoft.com/ManagedMediaHelpers

  本文將透過此Library,實做一個可透過WebClient下載,並經由NetworkStream直接進行Streaming Play的範例,此處將重用之前Audio Playback Agent範例中UI介面、Audio Playback Agent的大部分程式碼,如下所示:

 

MainPage.xaml.cs


using System;

using System.Collections.Generic;

using System.Linq;

using System.Net;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Documents;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Animation;

using System.Windows.Shapes;

using Microsoft.Phone.Controls;

using Microsoft.Phone.BackgroundAudio;

using System.IO;

using System.IO.IsolatedStorage;

using Microsoft.Xna.Framework.Media;


namespace StreamingAudioDemo

{

    public partial class MainPage : PhoneApplicationPage

    {


        private void CreateTrackList(string[] tracks)

        {

            using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())

            {

                using (IsolatedStorageFileStream fs = new IsolatedStorageFileStream("track.lst", FileMode.Create, FileAccess.Write, isf))

                {

                    using (StreamWriter sw = new StreamWriter(fs))

                    {

                        foreach (var track in tracks)

                            sw.WriteLine(track);

                    }

                }

            }

        }


        private void CreateResetCmd()

        {

            using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())

            {

                using (IsolatedStorageFileStream fs = new IsolatedStorageFileStream("reset.cmd", FileMode.Create, FileAccess.Write, isf))

                {

                    using (StreamWriter sw = new StreamWriter(fs))

                    {

                        sw.WriteLine("reset");

                    }

                }

            }

        }


        // Constructor

        public MainPage()

        {

            InitializeComponent();

            slider1.Value = BackgroundAudioPlayer.Instance.Volume;

            BackgroundAudioPlayer.Instance.PlayStateChanged += new EventHandler(Instance_PlayStateChanged);

            if (BackgroundAudioPlayer.Instance.PlayerState == PlayState.Playing)

                textBlock1.Text = BackgroundAudioPlayer.Instance.Track.Title;

        }


        void Instance_PlayStateChanged(object sender, EventArgs e)

        {

            if (BackgroundAudioPlayer.Instance.PlayerState == PlayState.Playing)

                textBlock2.Text = BackgroundAudioPlayer.Instance.Track.Title;

        }


        private void button1_Click(object sender, RoutedEventArgs e)

        {

            if (BackgroundAudioPlayer.Instance.PlayerState == PlayState.Stopped || BackgroundAudioPlayer.Instance.PlayerState == PlayState.Unknown)

            {

                string[] files = new string[] { "01.mp3", "02.mp3", "03.mp3", "04.mp3", "05.mp3" };

                CreateTrackList(files);

                CreateResetCmd();

            }

            if (BackgroundAudioPlayer.Instance.PlayerState != PlayState.Playing)

            {

                if (MediaPlayer.State == MediaState.Paused || MediaPlayer.State == MediaState.Playing)

                {

                    if (MessageBox.Show("media player is playing or paused, are you want stop it and play current choosed?",
                                                                   "video player", MessageBoxButton.OKCancel) == MessageBoxResult.Cancel)

                        return;

                }

                BackgroundAudioPlayer.Instance.Play();

            }


        }


        private void button2_Click(object sender, RoutedEventArgs e)

        {

            BackgroundAudioPlayer.Instance.SkipNext();

        }


        private void button3_Click(object sender, RoutedEventArgs e)

        {

            BackgroundAudioPlayer.Instance.SkipPrevious();

        }


        private void button4_Click(object sender, RoutedEventArgs e)

        {

            BackgroundAudioPlayer.Instance.Stop();

            CreateResetCmd();

            BackgroundAudioPlayer.Instance.Play();

        }


        private void slider1_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)

        {

            BackgroundAudioPlayer.Instance.Volume = slider1.Value;

        }

    }

}

AudioPlaybackAgent1.cs


using System;

using System.Windows;

using System.Collections;

using System.Collections.Generic;

using Microsoft.Phone.BackgroundAudio;

using System.IO;

using System.IO.IsolatedStorage;


namespace AudioPlaybackAgent1

{

    public class AudioPlayer : AudioPlayerAgent

    {

        private static volatile bool _classInitialized;

        private static List _tracks = new List();

        private static int _currentTrack = -1;


        ///

        ///AudioPlayer instances can share the same process.

        ///Static fields can be used to share state between AudioPlayer instances

        ///or to communicate with the Audio Streaming agent.

        ///

        public AudioPlayer()

        {

            if (!_classInitialized)

            {

                _classInitialized = true;

                // Subscribe to the managed exception handler

                Deployment.Current.Dispatcher.BeginInvoke(delegate

                {

                    Application.Current.UnhandledException += AudioPlayer_UnhandledException;

                });

            }

        }


        ///Code to execute on Unhandled Exceptions

        private void AudioPlayer_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e)

        {

            if (System.Diagnostics.Debugger.IsAttached)

            {

                // An unhandled exception has occurred; break into the debugger

                System.Diagnostics.Debugger.Break();

            }

        }


        private void LoadTrack()

        {

            using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())

            {

                using (IsolatedStorageFileStream fs = new IsolatedStorageFileStream("track.lst", FileMode.Open, FileAccess.Read, isf))

                {

                    _tracks.Clear();

                    using (StreamReader sr = new StreamReader(fs))

                    {

                        while (sr.Peek() != -1)

                            _tracks.Add(sr.ReadLine());

                    }

                }

            }

            _currentTrack = -1;

        }


        private bool NeedReset()

        {

            using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())

            {

                if (isf.FileExists("reset.cmd"))

                {

                    isf.DeleteFile("reset.cmd");

                    return true;

                }

            }

            return false;

        }



        protected override void OnPlayStateChanged(BackgroundAudioPlayer player, AudioTrack track, PlayState playState)

        {

            switch (playState)

            {

                case PlayState.TrackEnded:

                    player.Track = GetPreviousTrack();

                    break;

                case PlayState.TrackReady:

                    player.Play();

                    break;

                case PlayState.Shutdown:

                    // TODO: Handle the shutdown state here (e.g. save state)

                    break;

                case PlayState.Unknown:

                    break;

                case PlayState.Stopped:

                    break;

                case PlayState.Paused:

                    break;

                case PlayState.Playing:

                    break;

                case PlayState.BufferingStarted:

                    break;

                case PlayState.BufferingStopped:

                    break;

                case PlayState.Rewinding:

                    break;

                case PlayState.FastForwarding:

                    break;

            }


            NotifyComplete();

        }



        ///

        ///Called when the user requests an action using application/system provided UI

        ///

        ///The BackgroundAudioPlayer

        ///The track playing at the time of the user action

        ///The action the user has requested

        ///The data associated with the requested action.

        ///In the current version this parameter is only for use with the Seek action,

        ///to indicate the requested position of an audio track

        ///

        ///User actions do not automatically make any changes in system state; the agent is responsible

        ///for carrying out the user actions if they are supported.

        ///

        ///Call NotifyComplete() only once, after the agent request has been completed, including async callbacks.

        ///

        protected override void OnUserAction(BackgroundAudioPlayer player, AudioTrack track, UserAction action, object param)

        {

            switch (action)

            {

                case UserAction.Play:

                    if (NeedReset())

                    {

                        LoadTrack();

                        if (player.Track != null) //not reset

                            player.Track = null;

                    }

                    if (player.PlayerState != PlayState.Playing)

                        player.Play();

                    break;

                case UserAction.Stop:

                    player.Stop();

                    break;

                case UserAction.Pause:

                    player.Pause();

                    break;

                case UserAction.FastForward:

                    player.FastForward();

                    break;

                case UserAction.Rewind:

                    player.Rewind();

                    break;

                case UserAction.Seek:

                    player.Position = (TimeSpan)param;

                    break;

                case UserAction.SkipNext:

                    player.Track = GetNextTrack();

                    break;

                case UserAction.SkipPrevious:

                    AudioTrack previousTrack = GetPreviousTrack();

                    if (previousTrack != null)

                    {

                        player.Track = previousTrack;

                    }

                    break;

            }


            NotifyComplete();

        }



        ///

        ///Implements the logic to get the next AudioTrack instance.

        ///In a playlist, the source can be from a file, a web request, etc.

        ///

        ///

        ///The AudioTrack URI determines the source, which can be:

        ///(a) Isolated-storage file (Relative URI, represents path in the isolated storage)

        ///(b) HTTP URL (absolute URI)

        ///(c) MediaStreamSource (null)

        ///

        ///an instance of AudioTrack, or null if the playback is completed

        private AudioTrack GetNextTrack()

        {

            // TODO: add logic to get the next audio track

            if (_currentTrack < _tracks.Count - 1)

                _currentTrack++;

            else

                _currentTrack = 0;


            AudioTrack track = new AudioTrack(null, _tracks[_currentTrack], "", "", null);


            // specify the track


            return track;

        }



        ///

        ///Implements the logic to get the previous AudioTrack instance.

        ///

        ///

        ///The AudioTrack URI determines the source, which can be:

        ///(a) Isolated-storage file (Relative URI, represents path in the isolated storage)

        ///(b) HTTP URL (absolute URI)

        ///(c) MediaStreamSource (null)

        ///

        ///an instance of AudioTrack, or null if previous track is not allowed

        private AudioTrack GetPreviousTrack()

        {

            // TODO: add logic to get the previous audio track


            if (_currentTrack > 0)

                _currentTrack--;

            else

                _currentTrack = 0;


            AudioTrack track = new AudioTrack(null, _tracks[_currentTrack], "", "", null);


            return track;

        }


        ///

        ///Called whenever there is an error with playback, such as an AudioTrack not downloading correctly

        ///

        ///The BackgroundAudioPlayer

        ///The track that had the error

        ///The error that occured

        ///If true, playback cannot continue and playback of the track will stop

        ///

        ///This method is not guaranteed to be called in all cases. For example, if the background agent

        ///itself has an unhandled exception, it won't get called back to handle its own errors.

        ///

        protected override void OnError(BackgroundAudioPlayer player, AudioTrack track, Exception error, bool isFatal)

        {

            if (isFatal)

            {

                Abort();

            }

            else

            {

                NotifyComplete();

            }


        }


        ///

        ///Called when the agent request is getting cancelled

        ///

        ///

        ///Once the request is Cancelled, the agent gets 5 seconds to finish its work,

        ///by calling NotifyComplete()/Abort().

        ///

        protected override void OnCancel()

        {


        }

    }

}

特別注意一點,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的任務已經完成。