Add/Edit Task

This is part 3 of the series on building WP7 ToDo application with Telerik WP7 Control toolkit. Please refer to the "master" blog post for more details and links for the other part of the series.

The add/edit task screen is a standard form for adding and editing tasks. The more interesting things here are how the rich (voice, photo, location) fields for the tasks are specified. But first let’s see how to populate the “standard” fields. The design of the add/edit screen is specified below: 

We basically have a form field for each of the Task class properties. For the string properties we have a TextBox element, for the Boolean properties we have Checkboxes and for Enum or List type the properties of  we have a RadListPicker. By using a simple (but powerful) two-way databinding in Silverlight creating such forms is relatively easy. When the task is in edit mode we need to first retrieve the data from database. Again we are using the SterlingDB APIs.

Here is how the query looks like: 

if (NavigationContext.QueryString.ContainsKey("TaskId")) {
  int taskId;
  int.TryParse(NavigationContext.QueryString["TaskId"], out taskId);
  Task task = SterlingService.Current.Database.Query<Task, int>()
   .Where(delegate(TableKey<Task, int> key) { return key.Key == taskId; })
   .First<TableKey<Task, int>>()
   .LazyValue.Value;
  Debug.Assert(task != null, "Task should not be null");
}

 

First we need to get the id for the Task which we want to edit. The id is passed as a parameter to the "EditTask.xaml" page. By using the WP7 Navigation APIs we can easily retrieve the id by using the QueryString collection. After we have the taskId we can directly query the database and to retrieve the task info. SterlingDB is an object-oriented database and it will de-serialize the entity into the correct instance of the Task type.

Now that we have the Task instance we should set the DataContext of our form to be that Task:

this.DataContext = task;

 

Now by using data binding we can display the Task info in the form fields. Here is an extract of how the form is specified:

        <TextBlock Text="Name" Style="{StaticResource TextBoxHeaderStyle}"/>
  <TextBox x:Name="TaskName" MaxLength="50" Text="{Binding Name, Mode=TwoWay}"/>              
  
   <TextBlock Text="Due Date" Style="{StaticResource TextBoxHeaderStyle}"/>
  <telerikInput:RadDatePicker Value="{Binding DueDate, Mode=TwoWay}"/>
  <telerikInput:RadTimePicker Value="{Binding DueDate, Mode=TwoWay}" />
  
<TextBlock Text="Priority" Style="{StaticResource ListPickerHeaderStyle}"/>
<telerikInput:RadListPicker ItemsSource="{Binding Source={StaticResource TaskPriorities}, Path=Values}"  SelectedItem="{Binding Priority, Mode=TwoWay}" />
  
<TextBlock Text="Recurrence" Style="{StaticResource ListPickerHeaderStyle}"/>
<telerikInput:RadListPicker ItemsSource="{Binding Source={StaticResource TaskRecurrences}, Path=Values}" SelectedItem="{Binding Recurrence, Mode=TwoWay}"/>

 

Now let’s see what code is required to the save to item in database. But before saving to database we need to validate the form to make sure that the required fields are filled and then to save the info into database. This is how the whole SaveTask method is implemented:

private void SaveButton_Click(object sender, EventArgs e)
  {
   EnsureBindingIsApplied();
  
   if (!ValidateTask())
   {
    return;
   }
  
   DummyProjectViewModel selectedProject = ProjectPicker.SelectedItem as DummyProjectViewModel;
   if (selectedProject.Id > -1)
   {
    task.ProjectId = selectedProject.Id;
   }
  
   if (deletePhotoOnSave)
   {
    task.DeletePhoto();
   }
   else if (photoData != null)
   {
    task.AssignPhoto(photoData);
   }
  
   if (deleteVoiceMemoOnSave)
   {
    task.DeleteVoiceMemo();
   }
   else if (voiceData != null)
   {
    task.AssignVoiceMemo(voiceData);
   }
  
   task.Save();
  
   this.NavigateToNextPage();
  }

 

The first thing we do in this method is calling the EnsureBindingIsApplied() method. What this method do? The reason for this method is that when user types in a textbox and press a button from the ApplicationBar the textbox do not lose its focus – thus the binding in this textbox is not yet applied. This means that we will not get the latest text type by the user in the focused textbox. Here is the code that is executed in our save method that deals with this:

// when click on the app button the textboxes are not blurred and thus binding update is not triggered.
private void EnsureBindingIsApplied()
{
 object focusedElement = FocusManager.GetFocusedElement();
 if (focusedElement is TextBox)
 {
  (focusedElement as TextBox).GetBindingExpression(TextBox.TextProperty).UpdateSource();
 }
}

 

By using the code above we are forcing the binding to be updated manually. This way we are sure that our Task instance is up to date with the latest info the user has specified.
After this is done we can validate the task. In our case we just want to make sure that the task has a Name specified:

private bool ValidateTask()
  {
   if (String.IsNullOrEmpty(task.Name))
   {
    TaskName.Focus();
    return false;
   }
  
   return true;
  }

 

Now that we are sure that the Task is in valid state we can save it into Database. Here is what the Save method of the Task do:

public void Save()
{
 SterlingService.Current.Database.Save(this);
 SterlingService.Current.Database.Flush();
}

 

As I mentioned before SterlingDB is an object oriented database and it does a lot of things for you. In this case we are just giving the Task instance and all the serialization, triggers, etc. are handled automatically. The call to Flush method is requred in order to make sure that the task will be saved physically into the IsolatedStorage of the application. If you do not call the flush method all the changes to the Task will remain in memory.

Now for the more interesting part – these are the following phone specific features added to this application:

  • When specifying email the user can type a new email or to get an email from one of this contacts,
  • Same for the phone number – user can specify its own or to get a phone number from one of his saved contacts on the device,
  • Users can attach a photo to the task,
  • Users can attach a voice memo to the tasks,
  • Users can specify a location for the task. They can also pick the current location as an option.

Let’s start with the email chooser task. In WP7 API we have different Task Choosers. They all have a very similar interface and follow similar pattern – you instantiate a new Task Chooser and then you receive a callback when the data is available. Here is how to pick an email address from users contact list:

private void GetEmail_Click(object sender, RoutedEventArgs e)
{
   EmailAddressChooserTask emailAddressChooserTask = new EmailAddressChooserTask();
   emailAddressChooserTask.Completed += new EventHandler<EmailResult>(emailAddressChooserTask_Completed);
   emailAddressChooserTask.Show();
 }
  
 private void emailAddressChooserTask_Completed(object sender, EmailResult e)
 {
   if (e.TaskResult == TaskResult.OK)
   {
    task.Email = e.Email;
   }
 }

 

And that’s it – you have the email. The same goes for the phone number chooser:

private void AddPhoneButton_Click(object sender, RoutedEventArgs e)
{
   PhoneNumberChooserTask phoneNumberChooserTask = new PhoneNumberChooserTask();
   phoneNumberChooserTask.Completed += new EventHandler<PhoneNumberResult>(phoneNumberChooserTask_Completed);
   phoneNumberChooserTask.Show();
}
  
private void phoneNumberChooserTask_Completed(object sender, PhoneNumberResult e)
{
   if (e.TaskResult == TaskResult.OK)
   {
    task.Phone = e.PhoneNumber;
   }
}

 

Now these two tasks were easy. A little more effort is required when working with images. In our case we have the following requirements:

Here is the design requirement:

  • Choose a photo,
  • Preview the photo,
  • Have a functionality to replace or delete an existing photo.

To work with photos we have a special task called PhotoChooserTask() – very similar to the previous two chooser tasks. Here is the code:

bool isPhotoChooserTaskOpen = false;
private void ChooseNewPhoto()
{
   if (isPhotoChooserTaskOpen) return; // an exception will be thrown if we try to open the task more than once
   PhotoChooserTask photoChooserTask;
  
   photoChooserTask = new PhotoChooserTask();
   photoChooserTask.Completed += new EventHandler<PhotoResult>(photoChooserTask_Completed);
   isPhotoChooserTaskOpen = true;
   photoChooserTask.Show();
}
  
private void photoChooserTask_Completed(object sender, PhotoResult e)
{
   if (!this.isLoaded || e.TaskResult != TaskResult.OK)
   {
    return;
   }
   isPhotoChooserTaskOpen = false;
   photoData = new byte[e.ChosenPhoto.Length];
  
   e.ChosenPhoto.Read(photoData, 0, photoData.Length);
  
   BitmapImage image = new BitmapImage();
   image.SetSource(e.ChosenPhoto);
   deletePhotoOnSave = false;
   PhotoPreviewElement.Source = image;
   SetPhotoPreviewState();
}

 

As you can see the selection of photo is the same as in the previous tasks. After we got the photo we need to preview it using a thumbnail. For this purpose we need an Image element. We create a new BitmapImage and set is as a source to the Image element. In our scenario we not only need to preview the image but also to save the image data to the IsolatedStorage. This is done on save of the task. Here is the extract from the important logic:

if (deletePhotoOnSave)
{
    task.DeletePhoto();
}
else if (photoData != null)
{
    task.AssignPhoto(photoData);
}

 

Here is how the AssignPhoto and DeletePhoto methods look like:

public void AssignPhoto(byte[] photoData)
{
   this.DeletePhoto();
  
   string imgName = Guid.NewGuid().ToString();
   SaveAssetToLocalStorage(imgName, photoData);
   this.PhotoFileName = imgName;
}
  
public void DeletePhoto()
{
   if (String.IsNullOrEmpty(this.PhotoFileName))
   {
      return;
   }
   string filePath = System.IO.Path.Combine(AppModel.ASSETS_FOLDER, this.PhotoFileName);
   this.DeleteAsset(filePath);
   this.PhotoFileName = null;
  }
  
private void DeleteAsset(string assetFilePath)
{
   var isoFile = IsolatedStorageFile.GetUserStoreForApplication();
   if (isoFile.FileExists(assetFilePath))
   {
    isoFile.DeleteFile(assetFilePath);
   }
}
  
public void SaveAssetToLocalStorage(string fileName, byte[] content)
{
   string assetsFolder = AppModel.ASSETS_FOLDER;
   var isoFile = IsolatedStorageFile.GetUserStoreForApplication();
   if (!isoFile.DirectoryExists(assetsFolder))
   {
      isoFile.CreateDirectory(assetsFolder);
   }
  
   string filePath = System.IO.Path.Combine(assetsFolder, fileName);
   using (var stream = isoFile.CreateFile(filePath))
   {
      stream.Write(content, 0, content.Length);
   }
}

 

As you can see we are primarily working with the IsolatedStorage here. We create a new isolated storage file and we save its name in the Task instance in order to be able to retrieve the file later. I’ll not go into too much details – the code above should be fairly simple to understand.
Now – let’s continue with the voice memo. The requirements here are the same as the one for the photo. Users should be able to add/delete/replace and preview the voice memo. The logic implemented is exactly the same as in the photo examples. The only difference is that we are using other API’s for voice recording and voice preview. Here is the code that handles the voice recording in WP7:

private void StartVoiceMemoRecording()
{
   if (microphone.State == MicrophoneState.Started) return;
  
   microphone.BufferReady += Microfone_BufferReady;
   microphone.BufferDuration = TimeSpan.FromMilliseconds(200);
   voiceBuffer = new byte[microphone.GetSampleSizeInBytes(microphone.BufferDuration)];
   microphone.Start();
  
   SetVoiceMemoPreviewState();
}
  
private void StopVoiceMemo_Click(object sender, MouseButtonEventArgs e)
{
   if (microphone.State != MicrophoneState.Started) return;
   deleteVoiceMemoOnSave = false;
   microphone.Stop();
   microphone.BufferReady -= Microfone_BufferReady;
  
   voiceData = voiceStream.ToArray();
  
   voiceSoundEffect = new SoundEffect(voiceData, microphone.SampleRate, AudioChannels.Mono);
   voiceSoundEffect.Play();
  
   SetVoiceMemoPreviewState();
}
  
private void Microfone_BufferReady(object sender, EventArgs e)
{
   microphone.GetData(voiceBuffer);
   voiceStream.Write(voiceBuffer, 0, voiceBuffer.Length);
}

 

Working with the microphone in WP7 is easy – you just need to use the following methods and events:

  • call Microphone.Start() method
  • subscribe to Microphone.BufferReady event
  • call Microphone.Stop() method when user is done with the recording

In the BufferReady event you need to save the voice data into a buffer. Once you stop the microphone you can play the voice by using the SoundEffect class from XNA this way:

voiceSoundEffect = new SoundEffect(voiceData, microphone.SampleRate, AudioChannels.Mono);
voiceSoundEffect.Play();

 

It all looks pretty nice here, isn’t it? There is one caveat though – when you are using XNA code you need to register this code in your application class:

this.ApplicationLifetimeObjects.Add(new XNAAsyncDispatcher(TimeSpan.FromMilliseconds(50)));
  
public class XNAAsyncDispatcher : IApplicationService
{
  private DispatcherTimer frameworkDispatcherTimer;
  public XNAAsyncDispatcher(TimeSpan dispatchInterval)
  {
     this.frameworkDispatcherTimer = new DispatcherTimer();
     this.frameworkDispatcherTimer.Tick += new EventHandler(frameworkDispatcherTimer_Tick);
     this.frameworkDispatcherTimer.Interval = dispatchInterval;
  }
  void IApplicationService.StartService(ApplicationServiceContext context) { this.frameworkDispatcherTimer.Start(); }
  void IApplicationService.StopService() { this.frameworkDispatcherTimer.Stop(); }
  void frameworkDispatcherTimer_Tick(object sender, EventArgs e) { FrameworkDispatcher.Update(); }
}

 

You can read more info about this on MSDN - http://msdn.microsoft.com/library/ff842408.aspx

Again the data from the voice recording is saved into the IsolatedStorage just like we do it for the photos.

The last “rich” item in the Task details is the Task location. To accomplish this we are using the BingMap control which is coming with the WP7 SDK. Here is how to show a map into your WP7 application:

<phoneMaps:Map x:Name="BingMap1" MouseLeftButtonDown="BingMap1_MouseLeftButtonDown" ZoomBarVisibility="Visible" />

 

This is all you need in order to show the map. Now let’s see how to use the location the user has specified:

Point tapPoint = e.GetPosition(BingMap1);
  
GeoCoordinate loc = BingMap1.ViewportPointToLocation(tapPoint);

 

We get the mouse coordinate where the users has clicked on the map and the we use the ViewportPointToLocation(Point) method to get the location of the tap.

Now that we have the location we want to show the nice pushpin on the map:

mapPushpin = new Pushpin();
mapPushpin.Location = loc;
  
// Add the MapLayer to the Map so the "Pushpin" gets displayed
BingMap1.Children.Add(mapPushpin);

 

As you can see we create a new Pushpin element and add it to the Children collection of the map. Next we save the Latitude and the Longitude of the location to the Task instance.  To display the location in the preview task screen we are not using the BingMaps control, but we are reusing the Maps control that is available on the WP7 device. Here is how to show a location on the map:

public void ShowLocation()
{
   if (this.Latitude != 0 && this.Longitude != 0)
   {
      WebBrowserTask webBrowserTask = new WebBrowserTask();
      webBrowserTask.URL = "maps:" + this.Latitude + "%2C" + this.Longitude;
      webBrowserTask.Show();
   }
}

 

We are creating a new WebBrowserTask with a special protocol “maps:” and give the Latitude and the Longitude as parameters.

In the next blog post I’m going to explain how to use the Sterling DB


About the Author

Valio Stoychev

Valentin Stoychev (@ValioStoychev) for long has been part of Telerik and worked on almost every UI suite that came out of Telerik. Valio now works as a Product Manager and strives to make every customer a successful customer.

 

Comments