Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

[WP7] TodoList using Mango

The next WP7 major update, codenamed ‘Mango’, brings along many new APIs and possibilities for developing apps. In this article, I’m going to cover some of them through the making of a simple TodoList app. This app will use Linq to SQL, alternative tiles, background agents, and reminders.

Solution creation and data storage

First things first: let’s create a Visual Studio solution, called ‘MangoTaskList’. The main project’s type is ‘Windows Phone Application’. When creating the project, Visual Studio will inquire which WP7 version you’re targeting. Make sure to select ‘Windows Phone 7.1’ to enjoy Mango goodness.

image_thumb6

Immediately add a second project, of type ‘Windows Phone Class Library’. Name that project ‘MangoTaskList.DataModel’. It will make things easier when implementing the background agents. Don’t forget to add a reference to the main project.

For data storage, we’re going to use a SQL CE database, and query it using Linq to SQL. First, add a class called ‘Task’ to the ‘MangoTaskList.DataModel’ project. Then add the ‘[Table]’ attribute to tell Linq to SQL that this class must be mapped on a table. The class will inherit from two interfaces: ‘INotifyPropertyChanging’ and ‘INotifyPropertyChanged’. Implementing ‘INotifyPropertyChanging’ is recommended by the MSDN to reduce the memory footprint.

The ‘Task’ class will have a few properties to store the title, description, and due date. Also add a ‘Id’ property used as an internal id.

This class should now looks like:

   1: [Table]
   2: public class Task : INotifyPropertyChanging, INotifyPropertyChanged
   3: {
   4:     private int id;
   5:     private string title;
   6:     private string description;
   7:     private DateTime dueDate;
   8:  
   9:     [Column(IsPrimaryKey = true, IsDbGenerated = true, DbType = "INT NOT NULL Identity", CanBeNull = false, AutoSync = AutoSync.OnInsert)]
  10:     public int Id
  11:     {
  12:         get
  13:         {
  14:             return this.id;
  15:         }
  16:  
  17:         set
  18:         {
  19:             this.NotifyPropertyChanging("Id");
  20:             this.id = value;
  21:             this.NotifyPropertyChanged("Id");
  22:         }
  23:     }
  24:  
  25:     [Column]
  26:     public string Title
  27:     {
  28:         get
  29:         {
  30:             return this.title;
  31:         }
  32:  
  33:         set
  34:         {
  35:             this.NotifyPropertyChanging("Title");
  36:             this.title = value;
  37:             this.NotifyPropertyChanged("Title");
  38:         }
  39:     }
  40:  
  41:     [Column]
  42:     public string Description
  43:     {
  44:         get
  45:         {
  46:             return this.description;
  47:         }
  48:  
  49:         set
  50:         {
  51:             this.NotifyPropertyChanging("Description");
  52:             this.description = value;
  53:             this.NotifyPropertyChanged("Description");
  54:         }
  55:     }
  56:  
  57:     [Column]
  58:     public DateTime DueDate
  59:     {
  60:         get
  61:         {
  62:             return this.dueDate;
  63:         }
  64:  
  65:         set
  66:         {
  67:             this.NotifyPropertyChanging("DueDate");
  68:             this.dueDate = value;
  69:             this.NotifyPropertyChanged("DueDate");
  70:         }
  71:     }
  72:  
  73:     public event PropertyChangingEventHandler PropertyChanging;
  74:     public event PropertyChangedEventHandler PropertyChanged;
  75:     
  76:     protected void NotifyPropertyChanging(string propertyName)
  77:     {
  78:         var eventHandler = this.PropertyChanging;
  79:  
  80:         if (eventHandler != null)
  81:         {
  82:             eventHandler(this, new PropertyChangingEventArgs(propertyName));
  83:         }
  84:     }
  85:  
  86:     protected void NotifyPropertyChanged(string propertyName)
  87:     {
  88:         var eventHandler = this.PropertyChanged;
  89:  
  90:         if (eventHandler != null)
  91:         {
  92:             eventHandler(this, new PropertyChangedEventArgs(propertyName));
  93:         }
  94:     }
  95: }
  96:  

The ‘[Column]’ attribute indicates which properties must be stored in the table. We add  parameters to the attribute on the Id property to indicate that it’s a primary key stored in an identity field –to use the autoincrement feature.

Then we can add a new class in the DataModel project, called ‘TaskDataContext’. This class inherits from System.Data.Linq.DataContext. It stores a connection string, and exposes a ‘Task’ collection automatically mapped on the corresponding table by Linq to SQL.

   1: public class TaskDataContext : DataContext
   2: {
   3:     protected const string ConnectionString = "Data Source=isostore:/ToDo.sdf";
   4:  
   5:     public TaskDataContext()
   6:         : base(ConnectionString)
   7:     { 
   8:     }
   9:  
  10:     public Table<Task> Tasks;
  11: }

Since the connection string will never change, we use it directly in a parameterless constructor.

Everything is set up to access the database. We just make sure that it’s created at application’s startup, by adding a few lines at the end of the constructor in App.xaml.cs:

   1: using (var dataContext = new TaskDataContext())
   2: {
   3:     if (!dataContext.DatabaseExists())
   4:     {
   5:         dataContext.CreateDatabase();
   6:     }
   7: }

Data storage: checked. Now we can build an application on top of it.

User Interface and alternative tiles

How will our TodoList app works? Let’s make it simple. If the user launch the app, a page is displayed allowing to create a new task. The task is saved in the database, and a tile is pinned on the phone’s home screen. When clicking on the tile, the user lands on a page allowing to view the task details, and modify or delete it. To map a page to a tile, we’ll add a ‘TileId’ parameter to the Uri.

The appropriate controls are created in MainPage.xaml :

   1: <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
   2:     <Grid>
   3:         <Grid.ColumnDefinitions>
   4:             <ColumnDefinition Width="120" />
   5:             <ColumnDefinition />
   6:         </Grid.ColumnDefinitions>
   7:         <Grid.RowDefinitions>
   8:             <RowDefinition Height="90" />
   9:             <RowDefinition Height="150"/>
  10:             <RowDefinition Height="200"/>
  11:             <RowDefinition />
  12:             <RowDefinition />
  13:         </Grid.RowDefinitions>
  14:         
  15:         <TextBlock Grid.Row="0" Grid.Column="0" Text="Title : " HorizontalAlignment="Right" VerticalAlignment="Center" />
  16:         <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Path=TaskTitle, Mode=TwoWay}" HorizontalAlignment="Stretch" VerticalAlignment="Center" />
  17:  
  18:         <TextBlock Grid.Row="2" Grid.Column="0" Text="Description : " HorizontalAlignment="Right" VerticalAlignment="Center" />
  19:         <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Path=Description, Mode=TwoWay}" HorizontalAlignment="Stretch" VerticalAlignment="Center" Height="200"/>
  20:        
  21:         
  22:         <TextBlock Grid.Row="1" Grid.Column="0" Text="Due date : " HorizontalAlignment="Right" VerticalAlignment="Center" />
  23:  
  24:         <StackPanel Grid.Row="1" Grid.Column="1" >
  25:             <toolkit:DatePicker x:Name="DatePicker" Value="{Binding Path=DueDate, Mode=TwoWay}" />
  26:             <toolkit:TimePicker x:Name="TimePicker" Value="{Binding Path=DueTime, Mode=TwoWay}" />
  27:         </StackPanel>
  28:             
  29:         <StackPanel x:Name="PanelCreateTask" Grid.Row="3" Grid.Column="1" Orientation="Horizontal">
  30:             <Button x:Name="ButtonCreate" Content="Create" HorizontalAlignment="Left" VerticalAlignment="Top" Click="ButtonCreate_Click" />                    
  31:         </StackPanel>
  32:  
  33:         <StackPanel x:Name="PanelEditTask" Grid.Row="3" Grid.Column="1" Orientation="Horizontal">
  34:             <Button x:Name="ButtonSave" Content="Save" HorizontalAlignment="Left" VerticalAlignment="Top" Click="ButtonSave_Click" />
  35:             <Button x:Name="ButtonDelete" Content="Delete" HorizontalAlignment="Left" VerticalAlignment="Top" Click="ButtonDelete_Click" />
  36:         </StackPanel>
  37:     </Grid>
  38: </Grid>

Note that we’re using the DatePicker and TimePicker controls from the Silverlight Toolkit.

There’s two panels in this page. One with a ‘Create’ button, and the other with ‘Update’ and ‘Delete’ buttons. These panels will be shown respectively when the applications is launched normally or by clicking on a tile.

In the code-behind file, we set the datacontext and create properties for controls databinding. No need to make the page more complex with a separate ViewModel, as it’s just a demo of Mango features. We also add a property to store the TaskDataContext:

   1: public MainPage()
   2: {
   3:     this.DataContext = this;
   4:  
   5:     this.InitializeComponent();
   6:  
   7:     this.DueDate = DateTime.Now;
   8:     this.DueTime = this.DueDate;
   9: }
  10:  
  11: public Task Task { get; set; }
  12: public string TaskTitle { get; set; }
  13: public string Description { get; set; }
  14:  
  15: public DateTime DueDate { get; set; }
  16:  
  17: public DateTime DueTime { get; set; }
  18:  
  19: public DateTime DueDateTime
  20: {
  21:     get { return this.DueDate.Date.Add(this.DueTime - this.DueTime.Date); }
  22: }
  23:  
  24: protected TaskDataContext TaskDataContext { get; set; }
  25:  

When the main page is loaded, we instantiate the TaskDataContext if needed. We also set the visibility of the panels, depending on whether the user clicked on a tile or not. We can know how the application was launched by checking the ‘TaskId’ parameter in the Uri. If found, we load the appropriate task, using a Linq query on the TaskDataContext.

   1: protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
   2: {
   3:     base.OnNavigatedTo(e);
   4:  
   5:     if (this.TaskDataContext == null)
   6:     {
   7:         this.TaskDataContext = new TaskDataContext();
   8:     }
   9:  
  10:     if (this.NavigationContext.QueryString.ContainsKey("TaskId"))
  11:     {
  12:         this.PanelCreateTask.Visibility = Visibility.Collapsed;
  13:         this.PanelEditTask.Visibility = Visibility.Visible;
  14:  
  15:         int taskId = int.Parse(this.NavigationContext.QueryString["TaskId"]);
  16:  
  17:         if (this.Task == null || this.Task.Id != taskId)
  18:         {
  19:             var task = this.TaskDataContext.Tasks.First(t => t.Id == taskId);
  20:  
  21:             this.TaskTitle = task.Title;
  22:             this.Description = task.Description;
  23:             this.DueDate = task.DueDate;
  24:             this.DueTime = task.DueDate;
  25:  
  26:             this.Task = task;
  27:         }
  28:     }
  29:     else
  30:     {
  31:         this.PanelCreateTask.Visibility = Visibility.Visible;
  32:         this.PanelEditTask.Visibility = Visibility.Collapsed;
  33:     }
  34: }

Now, what’s happening when the user clicks on the buttons?

The ‘Create’ button creates the new task, stores it in the database, then pins an alternative tile on the home screen. To create the tile, we instantiate a ‘StandardTileData’ object, and fill up its properties as needed:

- Title : the title displayed on the front side of the tile
- BackTitle : the title displayed on the back side of the tile
- BackContent : the back side’s content
- Count : a number displayed on the front side of the tile 
- BackgroundImage : the front side’s background 
- BackBackgroundImage : the back side’s background

Then we call the ‘ShellTile.Create’ method, with two parameters: the ‘StandardTileData’ object and the Uri of the page the user lands when clicking on the tile.

To create the new task, we instantiate a new ‘Task’ object, fill its properties, then pass it to the ‘InsertOnSubmit’ method of the TaskDataContext. The changes will be applied when calling the ‘SubmitChanges’ method.

   1: private void ButtonCreate_Click(object sender, RoutedEventArgs e)
   2: {
   3:     var task = new Task { Title = this.TaskTitle, DueDate = this.DueDateTime, Description = this.Description };
   4:  
   5:     this.TaskDataContext.Tasks.InsertOnSubmit(task);
   6:     this.TaskDataContext.SubmitChanges();
   7:  
   8:     var tile = new StandardTileData
   9:     {
  10:         Title = task.Title,
  11:         BackTitle = task.Title,
  12:         BackContent = task.DueDate.ToString()
  13:     };
  14:  
  15:     ShellTile.Create(new Uri(string.Format("/MainPage.xaml?TaskId={0}", task.Id), UriKind.Relative), tile);
  16: }

The ‘Update’ button updates the task and the associated tile. To update the task, we just have to update the appropriate properties, then call the ‘SubmitChanges’ method of the TaskDataContext.

To update the tile, we need to retrieve it from the ‘ShellTile.ActiveTiles’ collection. We can identify the right tile using its Uri. Once the tile is retrieved, we call the ‘Update’ method with a ‘StandardTileData’ object – after setting every property we want to change.

   1: private void ButtonSave_Click(object sender, RoutedEventArgs e)
   2: {
   3:     this.Task.Title = this.TaskTitle;
   4:     this.Task.Description = this.Description;
   5:     this.Task.DueDate = this.DueDateTime;
   6:  
   7:     this.TaskDataContext.SubmitChanges();
   8:  
   9:     var tile = ShellTile.ActiveTiles.FirstOrDefault(t => t.NavigationUri.OriginalString.Contains("TaskId=" + this.Task.Id));
  10:  
  11:     if (tile != null)
  12:     {
  13:         var tileData = new StandardTileData
  14:         {
  15:             Title = this.Task.Title,
  16:             BackTitle = this.Task.Title,
  17:             BackContent = this.Task.DueDate.ToString()
  18:         };
  19:  
  20:         tile.Update(tileData);
  21:     }
  22:  
  23:     MessageBox.Show("Task updated");
  24: }

Finally, the ‘Delete’ buttons erases the task and the associated tile. For that purpose, we call the ‘DeleteOnSubmit’ method of the TaskDataContext and the ‘Delete’ method of the tile.

   1: private void ButtonDelete_Click(object sender, RoutedEventArgs e)
   2: {
   3:     int taskId = this.Task.Id;
   4:  
   5:     this.TaskDataContext.Tasks.DeleteOnSubmit(this.Task);
   6:  
   7:     this.TaskDataContext.SubmitChanges();
   8:  
   9:     var tile = ShellTile.ActiveTiles.FirstOrDefault(t => t.NavigationUri.OriginalString.Contains("TaskId=" + taskId));
  10:  
  11:     if (tile != null)
  12:     {
  13:         tile.Delete();
  14:     }
  15:  
  16:     this.NavigationService.GoBack();
  17: }

And we’re done with the UI! Now let’s bring some life to those tiles.

image_thumb5

 

BackgroundAgents

Mango brings a multitask system based on agents and services. Services are used to launch in the background a specific task, like downloading a file or ringing the alarm. Agents are more generic but also more limited, for battery life matters.They’re allowed either to execute a code during 15 seconds, or run a more CPU-intensive task when the phone is plugged in.

To show how agents works, we’re going to use one to change the tile color depending on the time remaining until the task is due. The tile is colored in green if there’s one day or more remaining, orange if less than 24 hours, and red if overdue. Unfortunately, we can’t dynamically generate a tile background without storing it on a server. So we’re going to create them beforehand and add them to the solution.For that purpose, we add a ‘Images’ folder to the solution, add the images in the folder, and set their ‘Build Action’ to ‘Content’ in the properties.

image_thumb3

We’re going to use the following pictures:

TaskGreen.png :

TaskGreen_thumb1

TaskOrange.png :

TaskOrange_thumb1

TaskRed.png :

TaskRed_thumb2

We also need ‘blank’ pictures for the tiles back side:

BackgroundGreen.png :

BackgroundGreen_thumb1

BackgroundOrange.png :

BackgroundOrange_thumb1

BackgroundRed.png :

BackgroundRed_thumb1

In the code, we start by adding a ‘ToShellTile’ method in the ‘Task’ object to create the tile:

   1: public ShellTileData ToShellTile()
   2: {
   3:     return new StandardTileData
   4:     {
   5:         Title = this.Title,
   6:         BackTitle = this.Title,
   7:         BackContent = this.DueDate.ToString(),
   8:         BackgroundImage = GetBackgroundUri(this.DueDate),
   9:         BackBackgroundImage = GetBackBackgroundUri(this.DueDate)
  10:     };
  11: }
  12:  
  13: public static Uri GetBackgroundUri(DateTime dueDate)
  14: {
  15:     var remaining = dueDate - DateTime.Now;
  16:  
  17:     if (remaining.TotalSeconds < 0)
  18:     {
  19:         return new Uri("Images/TaskRed.png", UriKind.Relative);
  20:     }
  21:     
  22:     if (remaining.Days < 1)
  23:     {
  24:         return new Uri("Images/TaskOrange.png", UriKind.Relative);
  25:     }
  26:    
  27:     return new Uri("Images/TaskGreen.png", UriKind.Relative);
  28: }
  29:  
  30: public static Uri GetBackBackgroundUri(DateTime dueDate)
  31: {
  32:     var remaining = dueDate - DateTime.Now;
  33:  
  34:     if (remaining.TotalSeconds < 0)
  35:     {
  36:         return new Uri("Images/BackgroundRed.png", UriKind.Relative);
  37:     }
  38:  
  39:     if (remaining.Days < 1)
  40:     {
  41:         return new Uri("Images/BackgroundOrange.png", UriKind.Relative);
  42:     }
  43:  
  44:     return new Uri("Images/BackgroundGreen.png", UriKind.Relative);
  45: }

Nothing tricky here, since we already saw how the ‘StandardTileData’ object works. There’s also a bit of logic to define which picture must be displayed depending on the remaining time.

Now we replace our old tile generation code by a call to the ‘ToShellTile’ method:

   1: private void ButtonSave_Click(object sender, RoutedEventArgs e)
   2: {
   3:     this.Task.Title = this.TaskTitle;
   4:     this.Task.Description = this.Description;
   5:     this.Task.DueDate = this.DueDateTime;
   6:  
   7:     this.TaskDataContext.SubmitChanges();
   8:  
   9:     var tile = ShellTile.ActiveTiles.FirstOrDefault(t => t.NavigationUri.OriginalString.Contains("TaskId=" + this.Task.Id));
  10:  
  11:     if (tile != null)
  12:     {
  13:         tile.Update(this.Task.ToShellTile());
  14:     }
  15:  
  16:     MessageBox.Show("Task updated");
  17: }
  18:  
  19: private void ButtonCreate_Click(object sender, RoutedEventArgs e)
  20: {
  21:     var task = new Task { Title = this.TaskTitle, DueDate = this.DueDateTime, Description = this.Description };
  22:  
  23:     this.TaskDataContext.Tasks.InsertOnSubmit(task);
  24:     this.TaskDataContext.SubmitChanges();
  25:  
  26:     ShellTile.Create(new Uri(string.Format("/MainPage.xaml?TaskId={0}", task.Id), UriKind.Relative), task.ToShellTile());
  27: }

At this point, when starting the app, the tiles are correctly colored but aren’t updated as the time flows. To take care of that part, we’re going to add a BackgroundAgent.

To use a BackgroundAgent, we add a new project of type ‘Windows Phone Task Scheduler Agent’ to the solution. We call it ‘SchedulerAgent’.

image_thumb8

In this new project, a ‘TaskScheduler’ class is automatically generated. It will contain the tile updating logic. Let’s add a reference from this project to the ‘DataModel’ project, then have a look at the TaskScheduler.

Inside, there’s only two methods:

- ‘OnInvoke’: the logic to execute in background
- ‘OnCancel’: executed if the task is cancelled

For this app, we’ll focus on the ‘OnInvoke’ method.

What do that BakgroundAgent needs to do? Browsing the tiles, checking the due date, and updating the tile’s picture if needed. To browse the tiles, we use the “ShellTile.ActiveTiles” collection. Unfortunately, only the ‘NavigationUri’ property is exposed by tiles retrieved this way. There’s no property allowing to easily store an id to retrieve it later. Therefore, we have to parse the Uri to get the value of the ‘TaskId’ parameter and use it to identify each tile.

I didn’t manager to find any public class in the WP7 framework I could use out of the box to parse the query string. So I dug a little in the internal classes using Reflector, and picked the code I needed:

   1: protected static Dictionary<string, string> ParseUri(string queryString)
   2: {
   3:     var dictionary = new Dictionary<string, string>(StringComparer.Ordinal);
   4:  
   5:     foreach (string str in queryString.Split("&".ToCharArray(), StringSplitOptions.RemoveEmptyEntries))
   6:     {
   7:         int index = str.IndexOf("=", StringComparison.Ordinal);
   8:  
   9:         if (index == -1)
  10:         {
  11:             dictionary.Add(str, string.Empty);
  12:         }
  13:         else
  14:         {
  15:             dictionary.Add(str.Substring(0, index), str.Substring(index + 1));
  16:         }
  17:     }
  18:  
  19:     return dictionary;
  20: }

This method takes the query string, parses it, and returns the result as a Dictionary. We can use it to extract the ‘TaskId’ parameter from the Uri:

   1: protected override void OnInvoke(ScheduledTask scheduledTask)
   2: {
   3:     foreach (var tile in ShellTile.ActiveTiles)
   4:     {
   5:         string[] fragments = tile.NavigationUri.OriginalString.Split('?');
   6:  
   7:         if (fragments.Length > 0)
   8:         {
   9:             var values = ParseUri(string.Join("?", fragments, 1, fragments.Length - 1));
  10:  
  11:             if (values.ContainsKey("TaskId"))
  12:             {
  13:                 int taskId = int.Parse(values["TaskId"]);
  14:             }
  15:         }
  16:     }
  17:  
  18:     NotifyComplete();
  19: }

Now that we have the task id, we still need the due date. I first tried to use Linq to SQL to retrieve it from the database, but that was a mistake. Even the simplest query threw an ‘OutOfMemory’ exception: when executing, a BackgroundAgent has only 5 MB of RAM available! We have to be extra-careful with such a limitation.

So, what can we do? We could duplicate the task data in a readable file stored in the isolated storage, but it kind of defeats the whole purpose of using Linq to SQL. In that case, we only need the due date, so we can just add it to the tile’s Uri. The tile’s creation code in the ButtonCreate_Click method is changed to:

   1: ShellTile.Create(new Uri(string.Format("/MainPage.xaml?TaskId={0}&DueDate={1}", task.Id, task.DueDate), UriKind.Relative), task.ToShellTile());

Now we can retrieve the due date from the Uri and update the tile accordingly. Note that the method ‘StandardTileData.Update’ only updates the property you filled, which is really convenient since we don’t have the task’s title and description here.

   1: protected override void OnInvoke(ScheduledTask scheduledTask)
   2: {
   3:     foreach (var tile in ShellTile.ActiveTiles)
   4:     {
   5:         string[] fragments = tile.NavigationUri.OriginalString.Split('?');
   6:  
   7:         if (fragments.Length > 0)
   8:         {
   9:             var values = ParseUri(string.Join("?", fragments, 1, fragments.Length - 1));
  10:  
  11:             if (values.ContainsKey("DueDate"))
  12:             {
  13:                 DateTime dueDate;
  14:  
  15:                 if (DateTime.TryParse(values["DueDate"], out dueDate))
  16:                 {
  17:                     var tileUpdate = new StandardTileData
  18:                     {
  19:                         BackgroundImage = Task.GetBackgroundUri(dueDate),
  20:                         BackBackgroundImage = Task.GetBackBackgroundUri(dueDate)
  21:                     };
  22:  
  23:                     tile.Update(tileUpdate);
  24:                 }
  25:             }
  26:         }
  27:     }
  28:  
  29:     NotifyComplete();
  30: }

The background agent is ready, all that’s left is to start it. We add a ‘StartPeriodicAgent’ method in the ‘MainPage.xaml.cs’ file, where we instantiate a ‘PeriodicTask’ object. An application can only create one agent of each type, so we need to make sure that we haven’t already created one.

   1: private static void StartPeriodicAgent()
   2: {
   3:     const string periodicTaskName = "MangoTaskList";
   4:  
   5:     if (ScheduledActionService.Find(periodicTaskName) == null)
   6:     {
   7:         var periodicTask = new PeriodicTask(periodicTaskName);
   8:         periodicTask.Description = "Updates the MangoTaskList application tiles.";
   9:         ScheduledActionService.Add(periodicTask);
  10:     }
  11: }

We add a call to this method in ‘ButtonCreate_Click’ :

   1: private void ButtonCreate_Click(object sender, RoutedEventArgs e)
   2: {
   3:     var task = new Task { Title = this.TaskTitle, DueDate = this.DueDateTime, Description = this.Description };
   4:  
   5:     this.TaskDataContext.Tasks.InsertOnSubmit(task);
   6:     this.TaskDataContext.SubmitChanges();
   7:  
   8:     StartPeriodicAgent();
   9:  
  10:     ShellTile.Create(new Uri(string.Format("/MainPage.xaml?TaskId={0}&DueDate={1}", task.Id, task.DueDate), UriKind.Relative), task.ToShellTile());
  11: }

It’s good to mention that the agent expires after 15 days. Therefore, in a real life application we would have to check at application startup if the agent has expired, and decide whether to restart it or not.

Testing a BackgroundAgent can be painful. When the agent is created, Visual Studio automatically detaches the debugger from the app and attaches it to the agent, which starts executing immediately. This way, you can easily debug the most obvious bugs. But for the second execution you’ll have to wait patiently during 30 minutes in front of your emulator. Let’s hope that Microsoft adds a way to reduce the time between two executions of the agent before the release of Mango.

If everything went smoothly, the tiles should now update automatically depending on the time remaining before due. Of course, the tiles are updated with a delay up to 30 minutes, depending on when the agent’s execution starts. We’re now going to see how to use the Reminder to notify the user when a task is due.

image_thumb11

 

Reminders

The ‘Reminder’ class is located in the ‘Microsoft.Phone.Scheduler’ namespace. A Reminder have a few parameters to set: an unique name, a text to display, a begin time and an end time, an occurrence, and the Uri which will be opened if the user click on the notification.

We already have all those pieces of information, so we can create our Reminder. We have to be careful when starting the reminder, since an exception will be thrown if the begin date is previous to the current date. To make sure, we make sure that the task’s due time is at least one minute in the future.

There’s something else that can be tricky. Let’s have a look at our ‘ButtonCreate_Click’ method:

   1: private void ButtonCreate_Click(object sender, RoutedEventArgs e)
   2: {
   3:     var task = new Task { Title = this.TaskTitle, DueDate = this.DueDateTime, Description = this.Description };
   4:  
   5:     this.TaskDataContext.Tasks.InsertOnSubmit(task);
   6:     this.TaskDataContext.SubmitChanges();
   7:  
   8:     StartPeriodicAgent();
   9:  
  10:     ShellTile.Create(new Uri(string.Format("/MainPage.xaml?TaskId={0}&DueDate={1}", task.Id, task.DueDate), UriKind.Relative), task.ToShellTile());
  11: }

The first thing I tried was to create the Reminder at the end of this method. That was a mistake, because the ‘ShellTile.Create’ method immediately displays the home page, with the newly pinned tile. Meanwhile, the ‘ButtonCreate_Click’ continues executing in background, and an exception is thrown indicating that a Reminder cannot be created from an app running in the background. Therefore, we have to ensure that we create the alternative tile at the very end of the method.

Here is the final ‘ButtonCreate_Click’ method with the Reminder:

   1: private void ButtonCreate_Click(object sender, RoutedEventArgs e)
   2: {
   3:     var task = new Task { Title = this.TaskTitle, DueDate = this.DueDateTime, Description = this.Description };
   4:  
   5:     this.TaskDataContext.Tasks.InsertOnSubmit(task);
   6:     this.TaskDataContext.SubmitChanges();
   7:  
   8:     StartPeriodicAgent();
   9:  
  10:     if (task.DueDate > DateTime.Now.AddMinutes(1))
  11:     {
  12:         var reminder = new Reminder("MangoTaskList Task " + task.Id);
  13:         reminder.Content = string.Format("Your task \"{0}\" is due now!", task.Title);
  14:         reminder.BeginTime = task.DueDate;
  15:         reminder.ExpirationTime = task.DueDate;
  16:         reminder.NavigationUri = new Uri(string.Format("/MainPage.xaml?TaskId={0}", task.Id), UriKind.Relative);
  17:         reminder.RecurrenceType = RecurrenceInterval.None;
  18:  
  19:         ScheduledActionService.Add(reminder);
  20:     }
  21:  
  22:     ShellTile.Create(new Uri(string.Format("/MainPage.xaml?TaskId={0}&DueDate={1}", task.Id, task.DueDate), UriKind.Relative), task.ToShellTile());
  23: }

Once the Reminder is created, we still have to update or delete it along with the tasks. You can retrieve a Reminder by its name, so we write a method to compute the reminder name from a task id, to avoid code duplication:

   1: private static string GetReminderName(int taskId)
   2: {
   3:     return "MangoTaskList Task " + taskId;
   4: }

To find and update Reminders, the ‘ScheduledActionService’ has the methods ‘Find’, ‘Replace’, and ‘Remove’, which are self-explanatory. Our update and delete methods thus become:

   1: private void ButtonDelete_Click(object sender, RoutedEventArgs e)
   2: {
   3:     int taskId = this.Task.Id;
   4:  
   5:     this.TaskDataContext.Tasks.DeleteOnSubmit(this.Task);
   6:  
   7:     this.TaskDataContext.SubmitChanges();
   8:  
   9:     string reminderName = GetReminderName(this.Task.Id);
  10:  
  11:     if (ScheduledActionService.Find(reminderName) != null)
  12:     {
  13:         ScheduledActionService.Remove(reminderName);
  14:     }
  15:  
  16:     var tile = ShellTile.ActiveTiles.FirstOrDefault(t => t.NavigationUri.OriginalString.Contains("TaskId=" + taskId));
  17:  
  18:     if (tile != null)
  19:     {
  20:         tile.Delete();
  21:     }
  22:  
  23:     this.ButtonDelete.IsEnabled = false;
  24:     this.ButtonSave.IsEnabled = false;
  25:  
  26:     MessageBox.Show("Task deleted");
  27: }
  28:  
  29: private void ButtonSave_Click(object sender, RoutedEventArgs e)
  30: {
  31:     this.Task.Title = this.TaskTitle;
  32:     this.Task.Description = this.Description;
  33:     this.Task.DueDate = this.DueDateTime;
  34:  
  35:     this.TaskDataContext.SubmitChanges();
  36:  
  37:     string reminderName = GetReminderName(this.Task.Id);
  38:  
  39:     var schedule = ScheduledActionService.Find(reminderName);
  40:  
  41:     if (schedule != null)
  42:     {
  43:         if (this.Task.DueDate > DateTime.Now.AddMinutes(1))
  44:         {
  45:             schedule.BeginTime = this.Task.DueDate;
  46:             ScheduledActionService.Replace(schedule);
  47:         }
  48:         else
  49:         {
  50:             ScheduledActionService.Remove(reminderName);
  51:         }
  52:     }
  53:     else
  54:     {
  55:         // Previous schedule has expired, create a new one
  56:         if (this.Task.DueDate > DateTime.Now.AddMinutes(1))
  57:         {
  58:             var reminder = new Reminder(reminderName);
  59:             reminder.Content = string.Format("Your task \"{0}\" is due now!", this.Task.Title);
  60:             reminder.BeginTime = this.Task.DueDate;
  61:             reminder.ExpirationTime = this.Task.DueDate;
  62:             reminder.NavigationUri = new Uri(string.Format("/MainPage.xaml?TaskId={0}", this.Task.Id), UriKind.Relative);
  63:             reminder.RecurrenceType = RecurrenceInterval.None;
  64:  
  65:             ScheduledActionService.Add(reminder);
  66:         }
  67:     }
  68:  
  69:     var tile = ShellTile.ActiveTiles.FirstOrDefault(t => t.NavigationUri.OriginalString.Contains("TaskId=" + this.Task.Id));
  70:  
  71:     if (tile != null)
  72:     {
  73:         tile.Update(this.Task.ToShellTile());
  74:     }
  75:  
  76:     MessageBox.Show("Task updated");
  77: }

After compilation and execution, the MangoTaskList app grants us with a little notification popup when a task is due:

image_thumb2

 

That concludes this article about some of the new features provided by Mango. There’s many more to discover and experiment with, so stay tuned!

Publié dimanche 19 juin 2011 13:52 par KooKiz
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

Commentaires

Pas de commentaires
Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- SharePoint : Bug sur la gestion des permissions et la synchronisation Office par Blog Technique de Romelard Fabrice le 07-10-2014, 11:35

- SharePoint 2007 : La gestion des permissions pour les Workflows par Blog Technique de Romelard Fabrice le 07-08-2014, 11:27

- TypeMock: mock everything! par Fathi Bellahcene le 07-07-2014, 17:06

- Coding is like Read par Aurélien GALTIER le 07-01-2014, 15:30

- Mes vidéos autour des nouveautés VS 2013 par Fathi Bellahcene le 06-30-2014, 20:52

- Recherche un passionné .NET par Tkfé le 06-16-2014, 12:22

- [CodePlex] Projet KISS Workflow Foundation lancé par Blog de Jérémy Jeanson le 06-08-2014, 22:25

- Etes-vous yOS compatible ? (3/3) : la feuille de route par Le blog de Patrick [MVP SharePoint] le 06-06-2014, 00:30

- [MSDN] Utiliser l'approche Contract First avec Workflow Foundation 4.5 par Blog de Jérémy Jeanson le 06-05-2014, 21:19

- [ #ESPC14 ] TH10 Moving mountains with SharePoint ! par Le blog de Patrick [MVP SharePoint] le 06-01-2014, 11:30