Tuesday, November 15, 2011

Simple WP7 Mango App for Background Tasks, Toast, and Tiles: Code Explanation

This is a continuation of this post, explaining how the sample app works.

image

Caution: This sample isn’t meant as an example of best patterns and practices. If you see something in the code you would never do, then don’t.

Solution File Organization

The app has three Visual Studio Projects: the app, the agent, and the shared code library between them.

clip_image001

One of the great things about Mango is that now the built-in Visual Studio Unit test functionality is available. By separating out the meat of the work into the SampleShared project, I can also focus my unit testing there.

SampleApp1

The app has a MainPage, a ViewModel, and the WMAppManifest.xml file modified to note my ExtendedTask of BackgroundServiceAgent.

clip_image002

The Agent is extremely simple with a call to the shared library, an update to the Tile, and a call to pop up Toast.

clip_image003

The meat of the work is in the SampleShared library.

clip_image004

The AgentMgr manages the background agent: add, remove, find, run now. It is only called from the app via these buttons:
· Start Agent,
· Stop Agent,
· Launch For Test.

The BackgroundAgentRESTCall is a containing class for all the true processing I want the agent to do. It is only called from the agent. For this sample app, it just reads and writes to the mutexed isolated storage file.

The FileIsoStorageMutex class is called by both the agent and the app via these buttons:
· Read ISO Storage,
· Write ISO Storage.

The best practices suggests mutex-protected storage:
"For one-direction communication where the foreground application writes and the agent only reads, we recommend using an isolated storage file with a Mutex. We recommend that you do not use IsolatedStorageSettings to communicate between processes because it is possible for the data to become corrupt."

TileNotification updates the app tile pinned to the start screen via the Update Tile button. It is also called by the agent. By putting the tile code in a separate assembly, I avoid the issue that I can’t update the tile from the UI thread. The toast notification is called by the agent.

Debug Tip: Use the Debug.WriteLine() liberally and keep an eye on the Output window. This is where you can see the background agent begin and end.

Dive into Code - App

The three main parts of the code for the app are the MainPage, the ViewModel, and the WMAppManifest. The MainPage uses the ViewModel which in turn calls over to the SampleShared library. The WMAppManifest, however, has a new section and the Visual Studio project creation for the app didn’t add it – you get to do that.

The tasks section and default task were already there. I added the extended task:
<Tasks>
      <DefaultTask  Name ="_default" NavigationPage="MainPage.xaml"/>
      <ExtendedTask Name="BgTask">
        <BackgroundServiceAgent Specifier="ScheduledTaskAgent"
                                Name="Sample Agent" Source="SampleAgent"
                                Type="SampleAgent.SampleAgent" />
      </ExtendedTask>
    </Tasks>
The TextFile1.txt file of the App project includes all the blog posts and web pages I read to research this topic. If you want other code examples, explanations, or related Microsoft MSDN library documentation, use the links found there.

 

Dive into Code - The Agent Code

The agent code may be new to you but my example is short and simple. The default OnInvoke() provided when I created the Visual Studio Background Agent project is the only code in that project.

public class SampleAgent : ScheduledTaskAgent
    {

        public String BackgroundPNG = "background.png";
        public String MainPageURLForToast = "/MainPage.xaml";
        public String ToastTitle = "Mutex:";
        
        /// <summary>
        /// Called by the system when there is work to be done
        /// </summary>
        /// <param name="Task">The task representing the work to be done</param>
        protected override void OnInvoke(ScheduledTask Task)
        {
            Debug.WriteLine("SampleAgent.SampleAgent.OnInvoke");

            //Here is where you put the meat of the work to be done by the agent
            IsoStorageData MutexedData = BackgroundAgentRESTCall.Call();

            //Toast Popups (processname + datetime in minutes)
            ToastNotifications.ShowToast(ToastTitle, MutexedData.LastProcessToTouchFile + "-" + MutexedData.LastTimeFileTouched.ToShortTimeString().ToString(), MainPageURLForToast);

            //Tile Pinned to Start screen (processname + datetime in minutes)
            // "2" signifies sampleagent
            TileNotifications.UpdateTile(MutexedData.LastProcessToTouchFile + "-" + MutexedData.LastTimeFileTouched.ToShortTimeString().ToString(), BackgroundPNG, 2);

            // this makes the agent run in a shorter cycle than 30 minutes
            // if the iso data setting is turned off (from the app)
            if (MutexedData.CycleAgentEveryMinute)
            {
                AgentMgr.RunDebugAgent();
            }

            //Required Background Agent Finisher
            NotifyComplete();
      }

    }

Shared Code library

The real work is in this file so I’m going to go through the four areas: isolated storage, toast, tiles, and agent. This is a great place to put code to grab data from the phone app which the agent doesn’t have access to. There are many unsupported APIs that the background agents can’t use.

Dive into Code - Shared Code - FileIsoStorageMutex.cs

FileIsoStorageMutex is the class that allows the app and agent to read and or write to the same isolated storage, effectively sharing data. Since the agent and app can wind up using the storage at the same time, I protect the file by a named mutex.

private static Mutex Mutex = new Mutex(false, "BackgroundAgentDemo1");


In order to create a mutex, you have to add a using statement of System.Threading which is contained in the mscorlib.Extensions.dll. Add the dll to the references section of the shared project.

In a production app, I would separate out the data. One isolated storage file would contain app-only data and a second, mutexed file would contain the data that is shared between the app and the agent. While I’m sure the mutex will protect the file sufficiently, I wouldn’t want to accidentally overwrite critical app data which might require a round-trip back to the server or input from the customer.

The code has two functions: read and write. Nothing tricky in either.
// Data object serialized to isolated storage
    public class IsoStorageData
    {
        //datetime of last write to iso storage
        public DateTime LastTimeFileTouched {get;set;}

        //app or agent
        public String LastProcessToTouchFile { get; set; }

        //true==continuous (every minute) cycle of agent
        public bool CycleAgentEveryMinute { get; set; }
    }

    // iso storage manager
    public static class MutexedIsoStorageFile
    {
        //named mutex
        private static Mutex Mutex = new Mutex(false, "BackgroundAgentDemo1");

        //name of isolated storage file
        private const String IsoStorageDateFile = "BackgroundAgentDemo1data.txt";

        //read iso storage
        //debug.writeline lets me "see" agent working in VS output window
        public static IsoStorageData Read()
        {
            IsoStorageData IsoStorageData = new IsoStorageData();

            Mutex.WaitOne();

            try
            {
                using (var store = IsolatedStorageFile.GetUserStoreForApplication())
                using (var stream = new IsolatedStorageFileStream(IsoStorageDateFile, FileMode.OpenOrCreate, FileAccess.Read, store))
                using (var reader = new StreamReader(stream))
                {
                    if (!reader.EndOfStream)
                    {
                        var serializer = new XmlSerializer(typeof(IsoStorageData));
                        IsoStorageData = (IsoStorageData)serializer.Deserialize(reader);
                    }
                }
            }
            finally
            {
                Mutex.ReleaseMutex();
            }
            Debug.WriteLine("RRR-data.LastProcessToTouchFile=" + IsoStorageData.LastProcessToTouchFile);
            Debug.WriteLine("RRR-data.LastTimeFileTouched=" + IsoStorageData.LastTimeFileTouched.ToString());
            Debug.WriteLine("RRR-data.CycleAgentEveryMinute=" + IsoStorageData.CycleAgentEveryMinute.ToString()); 
           
            return IsoStorageData;
        }
        //write iso storage
        //debug.writeline lets me "see" agent working in VS output window
        public static void Write(IsoStorageData data)
        {
            Debug.WriteLine("WWW-data.LastProcessToTouchFile=" + data.LastProcessToTouchFile);
            Debug.WriteLine("WWW-data.LastTimeFileTouched=" + data.LastTimeFileTouched.ToString());
            Debug.WriteLine("WWW-data.CycleAgentEveryMinute=" + data.CycleAgentEveryMinute.ToString());

            // persist the data using isolated storage
            using (var store = IsolatedStorageFile.GetUserStoreForApplication())
            using (var stream = new IsolatedStorageFileStream(IsoStorageDateFile,
                                                              FileMode.Create,
                                                              FileAccess.Write,
                                                              store))
            {
                var serializer = new XmlSerializer(typeof(IsoStorageData));
                serializer.Serialize(stream, data);
            }
        }

    }

Dive into Code - Shared Code - Tile Notifications

The default tile that is created as part of the app is used by this application. In a production app, I wouldn’t touch the tile from the app, opting instead to either use in-app notifications similar to the top, numeric indicator in the Facebook app or I would depend on the agent to alter the tile in the next 30 minute window. However, this is a sample so the app is directly updating the tile.
public static class TileNotifications
    {
        //update existing tile
        //no secondary tile
        //no animation
        //Title is title displayed on tile
        //background is can be changed but I use default image
        //good example of changed background image is the People Hub tile
        //count is the number to show in the little black circle
        public static void UpdateTile(String title, String backgroundImageUri, int count)
        {
            ShellTile firstTile = ShellTile.ActiveTiles.First();
            var newData = new StandardTileData() 
            { 
                Title = title, 
                BackgroundImage = new Uri(backgroundImageUri, UriKind.Relative),
                Count = count, 
            };

            firstTile.Update(newData);
        }
    }

The code doesn’t add more tiles, it doesn’t do anything with the backside of the tile, and it doesn’t have animations.

Dive into Code - Shared Code - Toast Notifications

Toast is only popped up via the agent but I wanted it in the shared app project so that I could have a single project for unit testing. The default Visual Studio Unit Test functionality is available to use in Mango while it wasn’t in Metro.
public static class ToastNotifications
    {
        //pop up toast
        //Title is the first part of the string - app name or reference key word
        //signifying app
        //Content is the second part of the string
        //Title + Content is how it is displayed
        //NavigationUri is the page inside your app that you want
        //the user taken to when they select/touch/click on your toast pop up
        //it doesn't have to be the main page
        public static void ShowToast(String title, String message, String NavigationURL)
        {
            var toast = new ShellToast
            {
                Title = title,
                Content = message,
                NavigationUri = new System.Uri(NavigationURL, System.UriKind.Relative)
            }; 
            
            toast.Show();
        }
    }


Dive into Code - Shared Code - BackgroundAgentRESTCall

The last class file contains the code that the agent calls to do the work. The code isn’t shared with the app in this sample, but in a production app I would have a single call that deals with notifications which the app and agent would share.
public static class BackgroundAgentRESTCall
    {
        public const String ProcessName = "Agent";

        public static IsoStorageData Call()
        {
            //This is where you put the call to your webservice. This
            //call can/should determine what to display on the tile and/or
            //toast. However, bring back as little as possible. Leave 
            //the bigger capture of data to your app.
 
            //For purposes of this sample, we just write/read iso storage 
            //and return results

            //Read the mutexed iso storage to get mutexedData.CycleAgentEveryMinute 
            IsoStorageData mutexedData = MutexedIsoStorageFile.Read();

            //Current datetime
            DateTime DateTimeNow = DateTime.Now;

            //Put current process and datetime into mutexed iso storage 
            //carry over previous cycle value
            MutexedIsoStorageFile.Write(new IsoStorageData()
            {
                LastProcessToTouchFile = ProcessName,
                LastTimeFileTouched = DateTimeNow,
                CycleAgentEveryMinute = mutexedData.CycleAgentEveryMinute 
            });

            //Read the mutexed iso storage to verify it was written
            mutexedData = MutexedIsoStorageFile.Read();

            return mutexedData;
        }
    }

Summary

This sample showed a simple app using a background agent to update the pinned tile and pop up toast. The .XAP binary and code solution are both available for download.   

No comments:

Post a Comment