다음을 통해 공유


How to implement background agents for Windows Phone 8

[ This article is for Windows Phone 8 developers. If you’re developing for Windows 10, see the latest documentation. ]

 

This topic walks you through implementing an app that uses a Scheduled Task to register a background agent.

Creating an app that uses Scheduled Tasks

The following steps walk you through creating a simple app that uses scheduled tasks to register a background agent.

To create an app that uses Scheduled Tasks

  1. In Visual Studio, create a new **Windows Phone App ** project. This template is in the Windows Phone category.

  2. Next, add a Scheduled Task project to your solution. From the File menu, select Add->New Project…. In the Add New Project dialog, select Windows Phone Scheduled Task Agent. Leave the default name, ScheduledTaskAgent1, and click OK.

  3. Next, in your foreground app project, you need to add a reference to the agent project. In SolutionExplorer, click your foreground app project to select it. Next, from the Project menu, select Add Reference…. In the Add Reference dialog, select the Projects tab. Select the agent project, ScheduledTaskAgent1, and click OK.

  4. Update your app manifest file to register your background agent. In Solution Explorer, under Properties, right-click the WMAppManifest.xml file and then click View Code. In the Tasks element, below the DefaultTask element, add the ExtendedTask element, shown in the following code. The Specifier attribute must have the value ScheduledTaskAgent. The other attributes should reflect the name you gave your scheduled task agent when you added it to the project in the preceding steps.

    <Tasks> 
      <DefaultTask Name="_default" NavigationPage="MainPage.xaml" /> 
      <ExtendedTask Name="BackgroundTask"> 
        <BackgroundServiceAgent Specifier="ScheduledTaskAgent" Name="ScheduledTaskAgent1" Source="ScheduledTaskAgent1" Type="ScheduledTaskAgent1.ScheduledAgent" /> 
      </ExtendedTask> 
    </Tasks> 
    
  5. In SolutionExplorer, double-click ScheduledAgent.cs under the ScheduledTaskAgent1 project to open the file. You will see that this file contains the definition for a single class, ScheduledAgent, which inherits from the base class ScheduledTaskAgent. For this example, add a using directive for the Shell namespace and the System namespace to the top of the file.

    using Microsoft.Phone.Scheduler;
    using Microsoft.Phone.Shell;
    using System;
    
  6. There is one method implemented in the class, OnInvoke(ScheduledTask). This method is called by the operating system when the Scheduled Task is launched. This is where you should place the code you want to execute when your background agent is run. Each app can have only one ScheduledTaskAgent registered at a time, but you can schedule this agent as both a resource-intensive agent and a periodic agent. If your app uses both a ResourceIntensiveTask and a PeriodicTask, you can check the type of the ScheduledTask object that is passed into the OnInvoke method to determine for which task the agent is being invoked and branch your code execution as necessary. If you use only one type of agent, you do not need to check the ScheduledTask object type. In this example, the agent launches a ShellToast object from OnInvoke, indicating the type of scheduled task for which the agent is being called. This toast will let you see when the agent is running. However, it will not be displayed while your foreground app is running.

    When your Scheduled Task code has completed, you should call NotifyComplete()()() to let the operating system know that you no longer need to execute. This allows the operating system to attempt to schedule other agents.

    protected override void OnInvoke(ScheduledTask task)
    {
      //TODO: Add code to perform your task in background
      string toastMessage = "";
    
      // If your application uses both PeriodicTask and ResourceIntensiveTask
      // you can branch your application code here. Otherwise, you don't need to.
      if (task is PeriodicTask)
      {
        // Execute periodic task actions here.
        toastMessage = "Periodic task running.";
      }
      else
      {
        // Execute resource-intensive task actions here.
        toastMessage = "Resource-intensive task running.";
      }
    
      // Launch a toast to show that the agent is running.
      // The toast will not be shown if the foreground application is running.
      ShellToast toast = new ShellToast();
      toast.Title = "Background Agent Sample";
      toast.Content = toastMessage;
      toast.Show();
    
      // If debugging is enabled, launch the agent again in one minute.
    #if DEBUG_AGENT
      ScheduledActionService.LaunchForTest(task.Name, TimeSpan.FromSeconds(60));
    #endif
    
      // Call NotifyComplete to let the system know the agent is done working.
      NotifyComplete();
    }
    

    The LaunchForTest(String, TimeSpan) method is provided so that you can run your agent on a more frequent schedule than it will run on an actual device. This method is only for app development. It will not function except for in apps that are deployed using the development tools. You should remove calls to this method from your production app. In this example, the call is placed in an #if block to enable you to easily switch between debugging and production functionality. To enable the call, put the following line at the top of the ScheduledAgent.csfile.

    #define DEBUG_AGENT
    
  7. In the next few steps, the foreground app is modified to allow the user to enable and disable the periodic and resource-intensive agents. First, open the MainPage.xaml file and paste the following XAML code within the Grid element named “ContentPanel”.

    <StackPanel>
      <StackPanel  Orientation="Vertical" Name="PeriodicStackPanel" Margin="0,0,0,40">
        <TextBlock Text="Periodic Agent" Style="{StaticResource PhoneTextTitle2Style}"/>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="name: " Style="{StaticResource PhoneTextAccentStyle}"/>
          <TextBlock Text="{Binding Name}" />
        </StackPanel>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="is enabled" VerticalAlignment="Center"  Style="{StaticResource PhoneTextAccentStyle}"/>
          <CheckBox Name="PeriodicCheckBox" IsChecked="{Binding IsEnabled}" Checked="PeriodicCheckBox_Checked" Unchecked="PeriodicCheckBox_Unchecked"/>                       
        </StackPanel>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="is scheduled: "  Style="{StaticResource PhoneTextAccentStyle}"/>
          <TextBlock Text="{Binding IsScheduled}" />
        </StackPanel>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="last scheduled time: "  Style="{StaticResource PhoneTextAccentStyle}"/>
          <TextBlock Text="{Binding LastScheduledTime}" />
        </StackPanel>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="expiration time: " Style="{StaticResource PhoneTextAccentStyle}"/>
          <TextBlock Text="{Binding ExpirationTime}" />
        </StackPanel>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="last exit reason: "  Style="{StaticResource PhoneTextAccentStyle}"/>
          <TextBlock Text="{Binding LastExitReason}" />
        </StackPanel>
      </StackPanel>
      <StackPanel  Orientation="Vertical" Name="ResourceIntensiveStackPanel" Margin="0,0,0,40">
        <TextBlock Text="Resource-intensive Agent" Style="{StaticResource PhoneTextTitle2Style}"/>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="name: " Style="{StaticResource PhoneTextAccentStyle}"/>
          <TextBlock Text="{Binding Name}" />
        </StackPanel>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="is enabled" VerticalAlignment="Center"  Style="{StaticResource PhoneTextAccentStyle}"/>
          <CheckBox Name="ResourceIntensiveCheckBox" IsChecked="{Binding IsEnabled}" Checked="ResourceIntensiveCheckBox_Checked" Unchecked="ResourceIntensiveCheckBox_Unchecked"/>
        </StackPanel>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="is scheduled: "  Style="{StaticResource PhoneTextAccentStyle}"/>
          <TextBlock Text="{Binding IsScheduled}" />
        </StackPanel>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="last scheduled time: "  Style="{StaticResource PhoneTextAccentStyle}"/>
          <TextBlock Text="{Binding LastScheduledTime}" />
        </StackPanel>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="expiration time: " Style="{StaticResource PhoneTextAccentStyle}"/>
          <TextBlock Text="{Binding ExpirationTime}" />
        </StackPanel>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="last exit reason: "  Style="{StaticResource PhoneTextAccentStyle}"/>
          <TextBlock Text="{Binding LastExitReason}" />
        </StackPanel>
      </StackPanel>
    </StackPanel>
    

    This code adds two sets of controls, one for each of the agent types. Most of the controls are text blocks that will be bound to the ScheduledTask objects that represent the background agents, allowing you to view the properties of these objects. Also, a checkbox is added for each agent type that displays and allows the user to toggle the enabled status of each agent. Handlers are added for the Checked and Unchecked events, which will be used to switch the agents on and off.

  8. In MainPage.xaml.cs, add a using directive for the Microsoft.Phone.Scheduler namespace to the top of the page.

    using Microsoft.Phone.Scheduler;
    
  9. Create two class variables that will represent each of the agent types. These are the objects that will be bound to the UI. The Scheduled Action Service uniquely identifies scheduled tasks by their Name property. Create two variables containing the names that will be used for the agents. Create one Boolean variable to track whether background agents have been disabled by the end user. Add these lines of code inside the class definition.

    public partial class MainPage : PhoneApplicationPage
    {
      PeriodicTask periodicTask;
      ResourceIntensiveTask resourceIntensiveTask;
    
      string periodicTaskName = "PeriodicAgent";
      string resourceIntensiveTaskName = "ResourceIntensiveAgent";
      public bool agentsAreEnabled = true;
    
  10. Next, implement a helper method called StartPeriodicAgent. This method first uses the Find(String) method to obtain a reference to the PeriodicTask with the specified name. If the scheduled task object is not null, then you should call Remove(String) to unregister the agent with the system. You cannot update agents directly. You must remove and then add. Next, create a new PeriodicTask object and assign its name in the constructor. Next, set the Description property. This property is required for Periodic agents and is used to describe the agent to the user in the background tasks Settings page on the device. Next, call Add(ScheduledAction) to register the Periodic agent with the system. An InvalidOperationException is thrown when Add is called if the user has disabled background agents for the app, so the call should be placed in a try block. If the call succeeds, set the data context of the associated UI element to update the data binding and display the objects properties to the user. If the call throws an exception, verify the message string of the exception. If the string is “BNS Error: The action is disabled”, then you should alert the user that background agents have been disabled.

    The LaunchForTest(String, TimeSpan) method is included to launch the agent one minute after this method call. As in the previous call to this method, it is placed in an #if block to allow the app to easily switch between debug and production modes. To enable this method, include the following line at the top of MainPage.xaml.cs.

    #define DEBUG_AGENT
    

    Paste the following method into the MainPage class definition.

    private void StartPeriodicAgent()
    {
      // Variable for tracking enabled status of background agents for this app.
      agentsAreEnabled = true;
    
      // Obtain a reference to the period task, if one exists
      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 debugging is enabled, use LaunchForTest to launch the agent in one minute.
    #if(DEBUG_AGENT)
        ScheduledActionService.LaunchForTest(periodicTaskName, TimeSpan.FromSeconds(60));
    #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;
        }
    
        if (exception.Message.Contains("BNS Error: The maximum number of ScheduledActions of this type have already been added."))
        {
          // No user action required. The system prompts the user when the hard limit of periodic tasks has been reached.
    
        }
        PeriodicCheckBox.IsChecked = false;
      }
      catch (SchedulerServiceException)
      {
        // No user action required.
        PeriodicCheckBox.IsChecked = false;
      }
    }
    
  11. Next, implement a start helper method for the resource-intensive task. This method is identical to the method for the periodic agent except it uses the ResourceIntensiveTask class to schedule the agent and a different name is used.

    private void StartResourceIntensiveAgent()
    {
      // Variable for tracking enabled status of background agents for this app.
      agentsAreEnabled = true;
    
      resourceIntensiveTask = ScheduledActionService.Find(resourceIntensiveTaskName) as ResourceIntensiveTask;
    
      // 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 (resourceIntensiveTask != null)
      {
        RemoveAgent(resourceIntensiveTaskName);
      }
    
      resourceIntensiveTask = new ResourceIntensiveTask(resourceIntensiveTaskName);
    
      // 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.
      resourceIntensiveTask.Description = "This demonstrates a resource-intensive task.";
    
      // Place the call to Add in a try block in case the user has disabled agents.
      try
      {
        ScheduledActionService.Add(resourceIntensiveTask);
        ResourceIntensiveStackPanel.DataContext = resourceIntensiveTask;
    
        // If debugging is enabled, use LaunchForTest to launch the agent in one minute.
    #if(DEBUG_AGENT)
        ScheduledActionService.LaunchForTest(resourceIntensiveTaskName, TimeSpan.FromSeconds(60));
    #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;
    
        }
        ResourceIntensiveCheckBox.IsChecked = false;
      }
      catch (SchedulerServiceException)
      {
        // No user action required.
        ResourceIntensiveCheckBox.IsChecked = false;
      }
    
    
    }
    
  12. Add a Boolean class variable, ignoreCheckBoxEvents. This variable will be used to switch off the CheckBox events when initializing the page.

    bool ignoreCheckBoxEvents = false;
    
  13. Now, add event handlers for the Checked and Unchecked events for CheckBox controls. These handlers call the start and stop helper methods created in the previous steps. If ignoreCheckBoxEvents is true, the handlers return without doing anything.

    private void PeriodicCheckBox_Checked(object sender, RoutedEventArgs e)
    {
      if (ignoreCheckBoxEvents) 
        return;
      StartPeriodicAgent();
    }
    private void PeriodicCheckBox_Unchecked(object sender, RoutedEventArgs e)
    {
      if (ignoreCheckBoxEvents)
       return;
      RemoveAgent(periodicTaskName);
    }
    private void ResourceIntensiveCheckBox_Checked(object sender, RoutedEventArgs e)
    {
      if (ignoreCheckBoxEvents)
        return;
      StartResourceIntensiveAgent();
    }
    private void ResourceIntensiveCheckBox_Unchecked(object sender, RoutedEventArgs e)
    {
      if (ignoreCheckBoxEvents)
        return;
      RemoveAgent(resourceIntensiveTaskName);
    }
    
  14. The RemoveAgent helper method simply calls Remove(String) in a try block so that an exception will not cause the app to exit.

    private void RemoveAgent(string name)
    {
      try
      {
        ScheduledActionService.Remove(name);
      }
      catch (Exception)
      {
      }
    }
    
  15. The final step is to override the OnNavigatedTo(NavigationEventArgs) method of the PhoneApplicationPage class. This method is called whenever the user navigates to the page. Set ignoreCheckBoxEvents to true so that the CheckBox event handlers are essentially disabled. Then, use the Find(String) method to see whether the app’s periodic and resource-intensive agents are registered with the system and update the CheckBox controls to reflect the current state of the app. Finally, set ignoreCheckBoxEvents back to false.

    protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
    {
      ignoreCheckBoxEvents = true;
    
      periodicTask = ScheduledActionService.Find(periodicTaskName) as PeriodicTask;
    
      if (periodicTask != null)
      {
        PeriodicStackPanel.DataContext = periodicTask;
      }
    
      resourceIntensiveTask = ScheduledActionService.Find(resourceIntensiveTaskName) as ResourceIntensiveTask;
      if (resourceIntensiveTask != null)
      {
        ResourceIntensiveStackPanel.DataContext = resourceIntensiveTask;
      }
    
      ignoreCheckBoxEvents = false;
    
    }