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:
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:
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:
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
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.