Tracking with Tiles and Toasts

This article shows a way to implement multiple alarm clock functionality in a Windows (Store or Enterprise) MVVM XAML App. We’ll use locally scheduled Toast and Tile Notifications to track the progress of a workflow - like a workout scheme or a cooking recipe. The workflow can be started, paused, restarted, and canceled by the user. The app will schedule, reschedule, and unschedule the appropriate Toast and Tile Notifications so that the process can be monitored by the end user on the machine, even when the app gets suspended. Here’s a screenshot of the sample app, it monitors a classic cooking recipe:

Start

When the app is started for the first time, it creates the workflow steps, and welcomes you with a regular toast. I use a helper class that can spawn three different toasts. These toasts are a replacement for the classic Info, Warning, and Error message boxes. Here’s the whole class, I already use it in several published Store apps:

    /// <summary>
    /// Issues Toast Notifications.
    /// </summary>
    public static class Toast
    {
        /// <summary>
        /// Shows the specified text in a toast.
        /// </summary>
        public static void Show(string title, string text)
        {
            Toast.Show(title, text, null);
        }

        /// <summary>
        /// Shows a toast with an info icon.
        /// </summary>
        public static void ShowInfo(string title, string text)
        {
            Toast.Show(title, text, "ms-appx:///Assets/Toasts/Wink.png");
        }

        /// <summary>
        /// Shows a toast with a warning icon.
        /// </summary>
        public static void ShowWarning(string title, string text)
        {
            Toast.Show(title, text, "ms-appx:///Assets/Toasts/Worried.png");
        }

        /// <summary>
        /// Shows a toast with an error icon.
        /// </summary>
        public static void ShowError(string title, string content)
        {
            Toast.Show(title, content, "ms-appx:///Assets/Toasts/Confused.png");
        }

        /// <summary>
        /// Shows a toast with the specified text and icon.
        /// </summary>
        private static void Show(string title, string content, string imagePath)
        {
            XmlDocument toastXml = GetToast(title, content, imagePath);

            ToastNotification toast = new ToastNotification(toastXml);
            ToastNotificationManager.CreateToastNotifier().Show(toast);
        }

        /// <summary>
        /// Gets the toast.
        /// </summary>
        private static XmlDocument GetToast(string title, string content, string imagePath)
        {
            string toastXmlString =
                "<toast>\n" +
                    "<visual>\n" +
                        "<binding template=\"ToastImageAndText02\">\n" +
                            "<image id=\"1\" src=\"" + imagePath + "\"/>\n" +
                            "<text id=\"1\">" + title + "</text>\n" +
                            "<text id=\"2\">" + content + "</text>\n" +
                        "</binding>\n" +
                    "</visual>\n" +
                "</toast>\n";

            XmlDocument toastXml = new XmlDocument();
            toastXml.LoadXml(toastXmlString);

            return toastXml;
        }
    }

Here’s the welcome toast call:

Toast.ShowInfo("Welcome", "I created some default alarms for you.");

And this is how the result looks like:

ToastAtStart

Toast Notifications can be displayed immediately, but they can also be scheduled for the future. Let’s take a look at that, and dive into some other helper classes.

Each step in the chicken recipe workflow is represented by an instance of the Alarm class – the ViewModel to a Toast Notification. An alarm is in one of these states:

    /// <summary>
    /// Alarm States.
    /// </summary>
    public enum AlarmStates
    {
        New,
        Scheduled,
        Paused,
        Canceled,
        Delivered
    }

The Alarm helper class comes with expected properties such as an Identifier, Title, Content, State, and TimeSpan. The TimeLeft is a calculated property:

        /// <summary>
        /// Gets the time left.
        /// </summary>
        public TimeSpan TimeLeft
        {
            get
            {
                switch (this.State)
                {
                    case AlarmStates.New:
                        return this.timeSpan;
                    case AlarmStates.Scheduled:
                        return this.timeSpan.Add(this.enabledAt - DateTime.Now);
                    case AlarmStates.Paused:
                        return this.timeSpan;
                    case AlarmStates.Canceled:
                        return TimeSpan.FromSeconds(0);
                    case AlarmStates.Delivered:
                        return TimeSpan.FromSeconds(0);
                    default:
                        return TimeSpan.FromSeconds(0);
                }
            }
        }

Each Alarm is scheduled when the workflow starts, and has to notify the user when its associated step starts. Here’s the code that schedules an alarm; we update its state and schedule a Toast Notification:

        /// <summary>
        /// Schedules this instance.
        /// </summary>
        public async Task Schedule()
        {
            if (this.state == AlarmStates.Scheduled || this.state == AlarmStates.Delivered)
            {
                // No action.
                return;
            }

            if (this.state == AlarmStates.Paused)
            {
                this.TimeSpan = this.TimeLeft;
            }

            this.enabledAt = DateTime.Now;
            this.ScheduleToast();
            this.State = AlarmStates.Scheduled;

            await RemovePersistedAlarm(this);
        }

The Toast Notification is scheduled through a ToastNotifier that registers a ScheduledToastNotification by calling AddToSchedule. For a introduction to all of these, check the alarm toast notifications sample on MSDN, which also demonstrates Snoozing and Dismiss functionality. Here’s the scheduling source code:

        /// <summary>
        /// The toast notifier
        /// </summary>
        private static ToastNotifier toastNotifier = ToastNotificationManager.CreateToastNotifier();

        /// <summary>
        /// Schedules the toast.
        /// </summary>
        private void ScheduleToast()
        {
            if (this.TimeSpan <= TimeSpan.FromSeconds(0))
            {
                return;
            }

            XmlDocument toastXml = GetToast();
            var toast = new ScheduledToastNotification(toastXml, DateTime.Now.Add(this.TimeSpan));
            toast.Id = this.Id;
            toastNotifier.AddToSchedule(toast);
        }

        /// <summary>
        /// Gets the toast.
        /// </summary>
        private XmlDocument GetToast()
        {
            string toastXmlString =
                "<toast duration=\"long\">\n" +
                    "<visual>\n" +
                        "<binding template=\"ToastImageAndText02\">\n" +
                            "<image id=\"1\" src=\"ms-appx:///Assets/Toasts/AlarmClock.png\"/>\n" +
                            "<text id=\"1\">" + this.title + "</text>\n" +
                            "<text id=\"2\">" + this.content + "</text>\n" +
                        "</binding>\n" +
                    "</visual>\n" +
                    "<audio src=\"ms-winsoundevent:Notification.Looping.Alarm2\" loop=\"true\" />\n" +
                "</toast>\n";

            XmlDocument toastXml = new XmlDocument();
            toastXml.LoadXml(toastXmlString);

            return toastXml;
        }

If you don’t like working with the raw XML version of the toast content, then I suggest you take a look at the NotificationsExtensions project. That project contains a more developer friendly wrapper around all of this.

A scheduled toast notification appears (and sounds) right on time, whether the app is active or not:

BasketToast

ToastNoApp

By the way: scheduled toast notifications –as well as scheduled tile notifications, see further- nicely survive restarts of your machine.

Back to the sample app. The entire workflow as such is not remembered. When the user stops and restarts the app, we recreate the list of alarms based on the scheduled toasts – that’s what GetScheduledToastNotifications does:

        /// <summary>
        /// Returns the scheduled alarms.
        /// </summary>
        public static IEnumerable<Alarm> ScheduledAlarms()
        {
            var toasts = toastNotifier.GetScheduledToastNotifications();

            return from t in toasts
                   select (Alarm)t;
        }

All we have to do is convert each native ScheduledToastNotification back to an instance of our Alarm ViewModel class. I used a conversion operator for that. If you’re dealing with the raw XML representation for the toasts, then you need to apply some XPATH magic (in SelectSingleNode) to get some of the data back:

        /// <summary>
        /// Performs an implicit conversion from <see cref="ScheduledToastNotification"/> to <see cref="Alarm"/>.
        /// </summary>
        public static implicit operator Alarm(ScheduledToastNotification toast)
        {
            Alarm result = new Alarm();

            result.Id = toast.Id;
            result.TimeSpan = toast.DeliveryTime - DateTime.Now;
            result.enabledAt = DateTime.Now;
            result.State = AlarmStates.Scheduled;

            var node = toast.Content.SelectSingleNode("//text[@id=1]");
            if (node != null)
            {
                result.Title = node.InnerText;
            }

            node = toast.Content.SelectSingleNode("//text[@id=2]");
            if (node != null)
            {
                result.Content = node.InnerText;
            }

            return result;
        }

The sample app allows an alarm to be paused, which is something that a native scheduled toast notification can’t handle. When an alarm is paused, we upgrade its state and remove the corresponding toast notification from the schedule:

        /// <summary>
        /// Pauses this instance.
        /// </summary>
        public async Task Pause()
        {
            if (this.state != AlarmStates.Scheduled)
            {
                // No action.
                return;
            }

            this.TimeSpan = this.TimeLeft;
            this.State = AlarmStates.Paused;
            this.UnscheduleToast();

            await AddPersistedAlarm(this);
        }

For unscheduling the toast, we look it up by its identifier through a LINQ query against GetScheduledToastNotifications, and remove it from the list with RemoveFromSchedule:

        /// <summary>
        /// Unschedules the toast.
        /// </summary>
        private void UnscheduleToast()
        {
            var toasts = toastNotifier.GetScheduledToastNotifications();
            var found = (from t in toasts
                         where t.Id == this.Id
                         select t).FirstOrDefault();
            if (found != null)
            {
                toastNotifier.RemoveFromSchedule(found);
            }
        }

For a paused alarm there’s no corresponding ScheduledToastNotification anymore, so when the app is suspended, it would be lost. To prevent this, we serialize the paused alarms as a list in the local folder so we can deserialize it when the app restarts. Here’s the corresponding code for all of this:

        /// <summary>
        /// The serializer
        /// </summary>
        private static AbstractSerializationBase<List<Alarm>> serializer =
            new XmlSerialization<List<Alarm>>()
            {
                FileName = "Alarms.xml",
                Folder = ApplicationData.Current.LocalFolder
            };

        /// <summary>
        /// Returns the persisted alarms.
        /// </summary>
        public static async Task<List<Alarm>> PersistedAlarms()
        {
            return await serializer.Deserialize();
        }

        /// <summary>
        /// Adds a persisted alarm.
        /// </summary>
        private static async Task AddPersistedAlarm(Alarm alarm)
        {
            var alarms = await PersistedAlarms();

            await RemovePersistedAlarm(alarm);

            alarm.TimeSpan = alarm.TimeLeft;
            alarms.Add(alarm);

            await serializer.Serialize(alarms);
        }

I reused the XML Serializer from this article. Because the native XML Serializer doesn’t handle the TimeSpan data type very well, I applied a little trick to serialize and deserialize the TimeSpan property. I created (serializable) shadow property that holds the number of Ticks in it:

        /// <summary>
        /// Gets or sets the time span.
        /// </summary>
        /// <remarks>Not XML serializable.</remarks>
        [XmlIgnore]
        public TimeSpan TimeSpan
        {
            get { return this.timeSpan; }
            set { this.SetProperty(ref this.timeSpan, value); }
        }

        /// <summary>
        /// Gets or sets the time span ticks.
        /// </summary>
        /// <remarks>Pretended property for serialization</remarks>
        [XmlElement("TimeSpan")]
        public long TimeSpanTicks
        {
            get { return this.timeSpan.Ticks; }
            set { this.timeSpan = new TimeSpan(value); }
        }

When an individual alarm is disabled, then we unschedule the corresponding toast notification, and remove it from the list of persisted alarms:

        /// <summary>
        /// Disables this instance.
        /// </summary>
        public async Task Disable()
        {
            if (this.state != AlarmStates.Scheduled && this.state != AlarmStates.Paused)
            {
                // No action.
                return;
            }

            this.UnscheduleToast();
            this.State = AlarmStates.Canceled;

            await RemovePersistedAlarm(this);
        }
        /// <summary>
        /// Unschedules the toast.
        /// </summary>
        private void UnscheduleToast()
        {
            var toasts = toastNotifier.GetScheduledToastNotifications();
            var found = (from t in toasts
                         where t.Id == this.Id
                         select t).FirstOrDefault();
            if (found != null)
            {
                toastNotifier.RemoveFromSchedule(found);
            }
        }
        /// <summary>
        /// Removes a persisted alarm.
        /// </summary>
        private static async Task RemovePersistedAlarm(Alarm alarm)
        {
            var alarms = await PersistedAlarms();

            alarms.RemoveAll(a => a.Id == alarm.Id);

            await serializer.Serialize(alarms);
        }

When the user starts the app by tapping on the tile, or on a toast, the app reassembles the (remaining) workflow by combining the scheduled toast notifications with the serialized paused alarms (we deliberately forget the steps for which the alarms were delivered). So this is how the app looks like after a restart:

Reopen

This is the code that brings back the list of alarms, it comes from the constructor of the main view model – which actually represents the workflow:

            // Create an alarm for all scheduled notifications from previous sessions that are still running.
            var scheduled = Alarm.ScheduledAlarms();

            if (scheduled.Count() > 0)
            {
                foreach (var alarm in scheduled)
                {
                    this.alarms.Add(alarm);
                }
            }

            CoreWindow.GetForCurrentThread().Dispatcher.RunAsync
                (
                    CoreDispatcherPriority.Normal,
                    async () =>
                    {
                        // Rehydrate paused alarms.
                        var persisted = await Alarm.PersistedAlarms();
                        if (persisted.Count() > 0)
                        {
                            foreach (var alarm in persisted)
                            {
                                this.alarms.Add(alarm);
                            }
                        }

                        // Create default alarms.
                        if (this.alarms.Count == 0)
                        {
                            Toast.ShowInfo("Welcome", "I created some default alarms for you.");
                            this.CreateDefaultAlarms();
                        }

                        this.RescheduleTiles();
                    }
                );

            DispatcherTimer toastTimer = new DispatcherTimer();
            toastTimer.Tick += this.ToastTimer_Tick;
            toastTimer.Interval = TimeSpan.FromSeconds(1);
            toastTimer.Start();

At the end of the previous code snippet, you see that we fire up a timer with a short interval (1 second). This timer updates the UI so the user has a detailed view on the workflow status through the changing TimeLeft fields. But it doesn’t stop here: we also want to give the user a high level –but less accurate- overview of the status of the running workflow. Therefor we decorate the app’s live tile with a message that contains the remaining time to the next notification and the remaining time for the entire flow. For that we schedule a regular tile update; every minute for the sample app, but I can imagine you would want a larger interval for a production app. Here’s the code to schedule these tile notifications, it’s very similar to the toast scheduling. This code is executed when the app restarts, when user modifies the flow, and also on a slow(er) moving timer.

For each minute between the current moment and the end of the workflow we lookup the next upcoming toast, and create the corresponding title and message on the live tile. The sample app notifies only the TimeLeft values, but you have access to all properties of the associated alarms (including Title and Content) if you want. Just remember that you only have three short lines of text for the tile:

        /// <summary>
        /// Reschedules the tiles.
        /// </summary>
        private void RescheduleTiles()
        {
            var scheduledAlarms = this.ScheduledAlarms;

            var nextAlarm = scheduledAlarms.FirstOrDefault();
            var lastAlarm = scheduledAlarms.LastOrDefault();

            if (nextAlarm == null)
            {
                // No alarms
                UnscheduleTileNotifications();
                return;
            }

            var next = (DateTime.Now + nextAlarm.TimeLeft) - nextAlarmTime;
            var nextMinutes = next.TotalMinutes;

            var last = (DateTime.Now + lastAlarm.TimeLeft) - lastAlarmTime;
            var lastMinutes = last.TotalMinutes;

            if ((Math.Abs(nextMinutes) < 2) && Math.Abs(lastMinutes) < 2)
            {
                // Nothing changed since the previous check.
                return;
            }

            nextAlarmTime = DateTime.Now.Add(nextAlarm.TimeLeft);
            lastAlarmTime = DateTime.Now.Add(lastAlarm.TimeLeft);

            UnscheduleTileNotifications();

            DateTime dateTime = DateTime.Now.AddSeconds(5);
            while (dateTime < lastAlarmTime)
            {
                string title = "Cooking";

                var alarm = (from a in scheduledAlarms
                             where a.TimeLeft > dateTime - DateTime.Now
                             select a).FirstOrDefault();

                if (alarm != null)
                {
                    string content;

                    if (alarm != lastAlarm)
                    {
                        content = String.Format(
                             "Notifies in {0} min.\nEnds in {1} min.",
                             (alarm.TimeLeft - (dateTime - DateTime.Now)).Minutes + 1,
                             (lastAlarm.TimeLeft - (dateTime - DateTime.Now)).Minutes + 1);
                    }
                    else
                    {
                        content = String.Format(
                             "Ends in {0} min.",
                             (lastAlarm.TimeLeft - (dateTime - DateTime.Now)).Minutes + 1);
                    }

                    tileUpdater.AddToSchedule(new ScheduledTileNotification(this.GetTile(title, content), dateTime));
                    Debug.WriteLine("Scheduled Tile Notification for {0}.", dateTime);
                }

                dateTime = dateTime.Add(TimeSpan.FromMinutes(1));
            }

            // Done.
            tileUpdater.AddToSchedule(new ScheduledTileNotification(this.GetTile("Job done", string.Empty), dateTime));
        }

Here’s an MS-Paintoshopped image of the evolution of the sample app's live tile throughout the workflow:

TileEvolution

The ‘Job Done’ tile notification is the last one. The tile remains like this until the app is restarted, since there seems to be no way to bring back the default tile, at least not through the scheduling infrastructure. If you really need this functionality, you could start a background job. Anyway, here’s the call to bring back the default tile:

//Reset to default tile.
tileUpdater.Clear();

The sample app uses a strictly sequential workflow, but the Alarm ViewModel class is reusable in more complex scenario’s with e.g. parallel tasks or tasks that can be paused individually.

Here’s the full source code, it was built with Visual Studio 2013 Update 2: U2UC.Win8.Alarms.zip (314.3KB)

Enjoy!

Diederik