Passwords & Secrets is a notepad-style app that you can protect with a master password. Therefore, it’s a great app for storing a variety of passwords and other secrets that you don’t want getting into the wrong hands. The note-taking functionality is top-notch, supporting
→ Auto-save, which makes jotting down notes fast and easy
→ Quick previews of each note
→ The ability to customize each note’s background/foreground colors and text size
→ The ability to email your notes
On top of this, the data in each note is encrypted with 256-bit Advanced Encryption Standard (AES) encryption to keep prying eyes from discovering the data. This encryption is done based on the master password, so it’s important that the user never forgets their password! There is no way for the app to retrieve the data without it, as the app does not store the password for security reasons.
To make management of the master password as easy as possible, Passwords & Secrets supports specifying and showing a password hint. It also enables you to change your password (but only if you know the current password).
Silverlight’s System.Security.Cryptography
namespace contains quite a bit of functionality for cryptographic tasks. This app wraps the necessary pieces of functionality from this namespace in order to expose an easy-to-use Crypto
class. This class exposes two simple methods—Encrypt
and Decrypt
—that accept the decrypted/encrypted data along with a password to use as the basis for the encryption and decryption. Listing 21.1 contains the implementation.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace WindowsPhoneApp
{
public static class Crypto
{
public static string Encrypt(string data, string password)
{
if (data == null)
return null;
using (SymmetricAlgorithm algorithm = GetAlgorithm(password))
using (MemoryStream memoryStream = new MemoryStream())
using (CryptoStream cryptoStream = new CryptoStream(
memoryStream, algorithm.CreateEncryptor(), CryptoStreamMode.Write))
{
// Convert the original data to bytes then write them to the CryptoStream
byte[] buffer = Encoding.UTF8.GetBytes(data);
cryptoStream.Write(buffer, 0, buffer.Length);
cryptoStream.FlushFinalBlock();
// Convert the encrypted bytes back into a string
return Convert.ToBase64String(memoryStream.ToArray());
}
}
public static string Decrypt(string data, string password)
{
if (data == null)
return null;
using (SymmetricAlgorithm algorithm = GetAlgorithm(password))
using (MemoryStream memoryStream = new MemoryStream())
using (CryptoStream cryptoStream = new CryptoStream(
memoryStream, algorithm.CreateDecryptor(), CryptoStreamMode.Write))
{
// Convert the encrypted string to bytes then write them
// to the CryptoStream
byte[] buffer = Convert.FromBase64String(data);
cryptoStream.Write(buffer, 0, buffer.Length);
cryptoStream.FlushFinalBlock();
// Convert the original data back to a string
buffer = memoryStream.ToArray();
return Encoding.UTF8.GetString(buffer, 0, buffer.Length);
}
}
// Hash the input data with a salt, typically used for storing a password
public static string Hash(string data)
{
// Convert the data to bytes
byte[] dataBytes = Encoding.UTF8.GetBytes(data);
// Create a new array with the salt bytes followed by the data bytes
byte[] allBytes = new byte[Settings.Salt.Value.Length + dataBytes.Length];
// Copy the salt at the beginning
Settings.Salt.Value.CopyTo(allBytes, 0);
// Copy the data after the salt
dataBytes.CopyTo(allBytes, Settings.Salt.Value.Length);
// Compute the hash for the combined set of bytes
byte[] hash = new SHA256Managed().ComputeHash(allBytes);
// Convert the bytes into a string
return Convert.ToBase64String(hash);
}
public static byte[] GenerateNewSalt(int length)
{
Byte[] bytes = new Byte[length];
// Fill the array with random bytes, using a cryptographic
// random number generator (RNG)
new RNGCryptoServiceProvider().GetBytes(bytes);
return bytes;
}
static SymmetricAlgorithm GetAlgorithm(string password)
{
// Use the Advanced Encryption Standard (AES) algorithm
AesManaged algorithm = new AesManaged();
// Derive an encryption key from the password
Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password,
Settings.Salt.Value);
// Initialize, converting the two values in bits to bytes (dividing by 8)
algorithm.Key = bytes.GetBytes(algorithm.KeySize / 8);
algorithm.IV = bytes.GetBytes(algorithm.BlockSize / 8);
return algorithm;
}
}
}
→ Both Encrypt
and Decrypt
call a GetAlgorithm
helper method (defined at the end of the file) to get started. The returned algorithm can create an encryptor or a decryptor, which is passed to a crypto stream that is used to drive the encryption/decryption work.
→ In Encrypt
, the input string is converted to bytes based on a UTF8 encoding. These bytes can then be written to the crypto stream to perform the encryption. The encrypted bytes are retrieved by using the ToArray
method on the underlying memory stream used by the crypto stream. These bytes are converted back to a stream using Base64 encoding, which is a common approach for representing binary data in a string.
→ Decrypt
starts with the Base64-encoded string and converts it to bytes to be written to the crypto stream. It then uses the underlying memory stream’s ToArray
method to convert the decrypted UTF8 bytes back into a string.
→ The Hash
function computes a SHA256 (Secure Hash Algorithm with a 256-bit digest) cryptographic hash of the input string prepended with a random “salt.” This is sometimes called a salted hash. This app calls this method in order to store a salted hash of the password rather than the password itself, for extra security. After all, if a hacker got a hold of the data in isolated storage, the encryption would be pointless if the password were stored along with it in plain text!
→ GenerateNewSalt
simply produces a random byte array of the desired length. Rather than using the simple Random
class used in other apps, this method uses RNGCryptoServiceProvider
, a higher-quality pseudo-random number generator that is more appropriate to use in cryptographic applications. As shown in the next section, this app calls this method only once, and only the first time the app is run. It stores the randomly generated salt in isolated storage and then uses that for all future encryption, decryption, and hashing.
→ GetAlgorithm
constructs the only built-in encryption algorithm, AesManaged
, which is the AES symmetric algorithm. The algorithm needs to be initialized with a secret key and an initialization vector (IV), so this is handled by the Rfc2898DeriveBytes
instance.
→ Rfc2898DeriveBytes
is an implementation of a password-based key derivation function—PBKDF2. This uses the password and a random “salt” value, and applies a pseudorandom function based on a SHA1 hash function many times (1000 by default). All this makes the password much harder to crack.
→ The default value of AesManaged
’s KeySize
property is also its maximum supported value: 256. This means that the key is 256-bits long, which is why this process is called 256-bit encryption.
LoginControl
User ControlWith the Crypto
class in place, we can create a login control that handles all the user interaction needed for the app’s master password. The LoginControl
user control used by this app is shown in Figure 21.1. It has three different modes:
→ The new user mode, in which the user must choose their master password for the first time
→ The normal login mode, in which the user must enter their previously chosen password
→ The change password mode, in which the user can change their password (after entering their existing password)
Listing 21.2 contains the XAML for this control.
<UserControl x:Class="WindowsPhoneApp.LoginControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WindowsPhoneApp"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}">
<Grid Background="{StaticResource PhoneBackgroundBrush}">
<!-- A dim accent-colored padlock image -->
<Rectangle Fill="{StaticResource PhoneAccentBrush}" Width="300" Height="364"
VerticalAlignment="Bottom" HorizontalAlignment="Right"
Margin="{StaticResource PhoneMargin}" Opacity=".5">
<Rectangle.OpacityMask>
<ImageBrush ImageSource="Images/lock.png"/>
</Rectangle.OpacityMask>
</Rectangle>
<ScrollViewer>
<Grid>
<!-- This panel is used for both New User and Change Password modes -->
<StackPanel x:Name="ChangePasswordPanel" Visibility="Collapsed"
Margin="{StaticResource PhoneMargin}">
<!-- Welcome! -->
<TextBlock x:Name="WelcomeTextBlock" Visibility="Collapsed"
Margin="{StaticResource PhoneHorizontalMargin}" TextWrapping="Wrap">
<Run FontWeight="Bold">Welcome!</Run>
<LineBreak/>
Choose a password that you'll remember. There is no way to recover ...
</TextBlock>
<!-- Old password -->
<TextBlock Text="Old password" x:Name="OldPasswordLabel"
Style="{StaticResource LabelStyle}"/>
<PasswordBox x:Name="OldPasswordBox" KeyUp="PasswordBox_KeyUp"/>
<!-- New password -->
<TextBlock Text="New password" Style="{StaticResource LabelStyle}"/>
<PasswordBox x:Name="NewPasswordBox" KeyUp="PasswordBox_KeyUp"/>
<!-- Confirm new password -->
<TextBlock Text="Type new password again"
Style="{StaticResource LabelStyle}"/>
<PasswordBox x:Name="ConfirmNewPasswordBox" KeyUp="PasswordBox_KeyUp"/>
<!-- Password hint -->
<TextBlock Text="Password hint (optional)"
Style="{StaticResource LabelStyle}"/>
<TextBox x:Name="PasswordHintTextBox" InputScope="Text"
KeyUp="PasswordBox_KeyUp"/>
<Button Content="ok" Click="OkButton_Click" MinWidth="226"
HorizontalAlignment="Left" Margin="0,12,0,0"
local:Tilt.IsEnabled="True"/>
</StackPanel>
<!-- This panel is used only for the Normal Login mode -->
<StackPanel x:Name="NormalLoginPanel" Visibility="Collapsed"
Margin="{StaticResource PhoneMargin}">
<TextBlock Text="Enter your password"
Style="{StaticResource LabelStyle}"/>
<PasswordBox x:Name="NormalLoginPasswordBox" KeyUp="PasswordBox_KeyUp"/>
<Button Content="ok" Click="OkButton_Click" MinWidth="226"
HorizontalAlignment="Left" local:Tilt.IsEnabled="True"/>
</StackPanel>
</Grid>
</ScrollViewer>
</Grid>
</UserControl>
Notes:
→ This control uses a password box wherever a password should be entered. A password box is just like a text box, except that it displays each character as a circle (after a brief moment in which you see the letter you just typed). This matches the behavior of password entry in all the built-in apps. Instead of a Text
property, it has a Password
property.
→ The opacity mask trick from Chapter 17, “Pick a Card Magic Trick,” is used to give an image of a padlock the color of the current theme’s accent color. It’s also given 50% opacity, so it blends into the background a bit more.
Listing 21.3 contains the code-behind.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace WindowsPhoneApp
{
public partial class LoginControl : UserControl
{
// A custom event
public event EventHandler Closed;
public LoginControl()
{
InitializeComponent();
// Update the UI depending on which of the three modes we're in
if (Settings.HashedPassword.Value == null)
{
// The "new user" mode
this.WelcomeTextBlock.Visibility = Visibility.Visible;
this.OldPasswordLabel.Visibility = Visibility.Collapsed;
this.OldPasswordBox.Visibility = Visibility.Collapsed;
this.ChangePasswordPanel.Visibility = Visibility.Visible;
}
else if (CurrentContext.IsLoggedIn)
{
// The "change password" mode
this.ChangePasswordPanel.Visibility = Visibility.Visible;
}
else
{
// The "normal login" mode
this.NormalLoginPanel.Visibility = Visibility.Visible;
}
}
void OkButton_Click(object sender, RoutedEventArgs e)
{
string currentHashedPassword = Settings.HashedPassword.Value;
if (currentHashedPassword != null && !CurrentContext.IsLoggedIn)
{
// We're in "normal login" mode
// If the hash of the attempted password matches the stored hash,
// then we know the user entered the correct password.
if (Crypto.Hash(this.NormalLoginPasswordBox.Password)
!= currentHashedPassword)
{
MessageBox.Show("", "Incorrect password", MessageBoxButton.OK);
return;
}
// Keep the unencrypted password in-memory,
// only until this app is deactivated/closed
CurrentContext.Password = this.NormalLoginPasswordBox.Password;
}
else
{
// We're in "new user" or "change password" mode
// For "change password," be sure that the old password is correct
if (CurrentContext.IsLoggedIn && Crypto.Hash(this.OldPasswordBox.Password)
!= currentHashedPassword)
{
MessageBox.Show("", "Incorrect old password", MessageBoxButton.OK);
return;
}
// Now validate the new password
if (this.NewPasswordBox.Password != this.ConfirmNewPasswordBox.Password)
{
MessageBox.Show("The two passwords don't match. Please try again.",
"Oops!", MessageBoxButton.OK);
return;
}
string newPassword = this.NewPasswordBox.Password;
if (newPassword == null || newPassword.Length == 0)
{
MessageBox.Show("The password cannot be empty. Please try again.",
"Nice try!", MessageBoxButton.OK);
return;
}
// Store a hash of the password so we can check for the correct
// password in future logins without storing the actual password
Settings.HashedPassword.Value = Crypto.Hash(newPassword);
// Store the password hint as plain text
Settings.PasswordHint.Value = this.PasswordHintTextBox.Text;
// Keep the unencrypted password in-memory,
// only until this app is deactivated/closed
CurrentContext.Password = newPassword;
// If there already was a password, we must decrypt all data with the old
// password (then re-encrypt it with the new password) while we still
// know the old password! Otherwise the data will be unreadable!
if (currentHashedPassword != null)
{
// Each item in the NotesList setting has an EncryptedContent property
// that must be processed
for (int i = 0; i < Settings.NotesList.Value.Count; i++)
{
// Encrypt with the new password the data that is decrypted
// with the old password
Settings.NotesList.Value[i].EncryptedContent =
Crypto.Encrypt(
Crypto.Decrypt(Settings.NotesList.Value[i].EncryptedContent,
this.OldPasswordBox.Password),
newPassword
);
}
}
}
CurrentContext.IsLoggedIn = true;
Close();
}
void PasswordBox_KeyUp(object sender, KeyEventArgs e)
{
// Allow the Enter key to cycle between text boxes and to press the ok
// button when on the last text box
if (e.Key == Key.Enter)
{
if (sender == this.PasswordHintTextBox ||
sender == this.NormalLoginPasswordBox)
OkButton_Click(sender, e);
else if (sender == this.OldPasswordBox)
this.NewPasswordBox.Focus();
else if (sender == this.NewPasswordBox)
this.ConfirmNewPasswordBox.Focus();
else if (sender == this.ConfirmNewPasswordBox)
this.PasswordHintTextBox.Focus();
}
}
public void Close()
{
if (this.Visibility == Visibility.Collapsed)
return; // Already closed
// Clear all
this.OldPasswordBox.Password = "";
this.NewPasswordBox.Password = "";
this.ConfirmNewPasswordBox.Password = "";
this.NormalLoginPasswordBox.Password = "";
this.PasswordHintTextBox.Text = "";
// Close by becoming invisible
this.Visibility = Visibility.Collapsed;
// Raise the event
if (this.Closed != null)
this.Closed(this, EventArgs.Empty);
}
}
}
Notes:
→ This listing makes use of some of the following settings defined in a separate Settings.cs
file:
public static class Settings
{
// Password-related settings
public static readonly Setting<byte[]> Salt =
new Setting<byte[]>("Salt", Crypto.GenerateNewSalt(16));
public static readonly Setting<string> HashedPassword =
new Setting<string>("HashedPassword", null);
public static readonly Setting<string> PasswordHint =
new Setting<string>("PasswordHint", null);
// The user's data
public static readonly Setting<ObservableCollection<Note>> NotesList =
new Setting<ObservableCollection<Note>>("NotesList",
new ObservableCollection<Note>());
// User settings
public static readonly Setting<bool> MakeDefault =
new Setting<bool>("MakeDefault", false);
public static readonly Setting<Color> ScreenColor =
new Setting<Color>("ScreenColor", Color.FromArgb(0xFF, 0xFE, 0xCF, 0x58));
public static readonly Setting<Color> TextColor =
new Setting<Color>("TextColor", Colors.Black);
public static readonly Setting<int> TextSize = new Setting<int>("TextSize",
22);
// Temporary state
public static readonly Setting<int> CurrentNoteIndex =
new Setting<int>("CurrentNoteIndex", -1);
public static readonly Setting<Color?> TempScreenColor =
new Setting<Color?>("TempScreenColor", null);
public static readonly Setting<Color?> TempTextColor =
new Setting<Color?>("TempTextColor", null);
}
The salt required by Rfc2898DeriveBytes
used by the Crypto
class must be at least 8 bytes. With the call to GenerateNewSalt
, this app generates a 16-byte salt.
→ In the normal login mode, the control must determine whether the entered password is correct. But the app doesn’t store the user’s password. Instead, it stores a salted hash of the password. Therefore, to validate the entered password, it calls the same Crypto.Hash
function and checks if it matches the stored hashed value.
→ Although the unencrypted password is not persisted, it is kept in memory while the app runs so it can decrypt the user’s saved content and encrypt any new content. This is done with the CurrentContext
class, defined as follows in CurrentContext.cs
:
public static class CurrentContext
{
public static bool IsLoggedIn = false;
public static string Password = null;
}
→ In the change password mode, something very important must be done before the old password is forgotten. Everything that has been encrypted with the old password must be decrypted then re-encrypted with the new password. Otherwise, the data would become unreadable because the new password cannot be used to decrypt data that was encrypted with the old password!
→ Inside Close
, the Password
property of each password box is set to an empty string instead of null
because the Password
property throws an exception if set to null
.
→ You can see that LoginControl
is not a general-purpose control but rather tailored to this app. (Although it wouldn’t be hard to generalize it by providing a hook for the consumer to perform the data re-encryption during the password-change process.) It is used in three separate places, shown in the next three sections of this chapter.
The change password page, seen previously in Figure 21.1, is nothing more than a page hosting a LoginControl
instance. The user can only reach this page when already signed in, so the control is automatically initialized to the “change password” mode thanks to the code in Listing 21.3. Listings 21.4 and 21.5 contain the simple XAML and code-behind for the change password page.
<phone:PhoneApplicationPage
x:Class="WindowsPhoneApp.ChangePasswordPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:local="clr-namespace:WindowsPhoneApp"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
SupportedOrientations="PortraitOrLandscape" shell:SystemTray.IsVisible="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- The standard header -->
<StackPanel Style="{StaticResource PhoneTitlePanelStyle}">
<TextBlock Text="PASSWORDS & SECRETS"
Style="{StaticResource PhoneTextTitle0Style}"/>
<TextBlock Text="change password"
Style="{StaticResource PhoneTextTitle1Style}"/>
</StackPanel>
<!-- The user control -->
<local:LoginControl Grid.Row="1" Closed="LoginControl_Closed"/>
</Grid>
</phone:PhoneApplicationPage>
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class ChangePasswordPage : PhoneApplicationPage
{
public ChangePasswordPage()
{
InitializeComponent();
}
void LoginControl_Closed(object sender, System.EventArgs e)
{
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
}
}
This app’s main page contains the list of user’s notes, as demonstrated in Figure 21.2. Each one can be tapped to view and/or edit it. A button on the application bar enables adding new notes. But before the list is populated and any of this is shown, the user must enter the correct password. When the user isn’t logged in, the LoginControl
covers the entire page except its header, and the application bar doesn’t have the add-note button.
Listing 21.6 contains the XAML for the main page.
<phone:PhoneApplicationPage
x:Class="WindowsPhoneApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:local="clr-namespace:WindowsPhoneApp"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
SupportedOrientations="PortraitOrLandscape" shell:SystemTray.IsVisible="True">
<phone:PhoneApplicationPage.Resources>
<local:DateConverter x:Key="DateConverter"/>
</phone:PhoneApplicationPage.Resources>
<!-- The application bar, with 3 menu items -->
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text="show password hint"
Click="PasswordMenuItem_Click"/>
<shell:ApplicationBarMenuItem Text="instructions"
Click="InstructionsMenuItem_Click"/>
<shell:ApplicationBarMenuItem Text="about" Click="AboutMenuItem_Click"/>
<shell:ApplicationBarMenuItem Text="more apps"
Click="MoreAppsMenuItem_Click"/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- The standard header -->
<StackPanel Grid.Row="0"
Style="{StaticResource PhoneTitlePanelStyle}">
<TextBlock Text="PASSWORDS & SECRETS"
Style="{StaticResource PhoneTextTitle0Style}"/>
</StackPanel>
<!-- Show this when there are no notes -->
<TextBlock Name="NoItemsTextBlock" Grid.Row="1" Text="No notes"
Visibility="Collapsed" Margin="22,17,0,0"
Style="{StaticResource PhoneTextGroupHeaderStyle}"/>
<!-- The list box containing notes -->
<ListBox x:Name="ListBox" Grid.Row="1" ItemsSource="{Binding}"
SelectionChanged="ListBox_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<!-- The title, in a style matching the note -->
<Border Background="{Binding ScreenBrush}" Margin="24,0" Width="800"
MinHeight="60" local:Tilt.IsEnabled="True">
<TextBlock Text="{Binding Title}" FontSize="{Binding TextSize}"
Foreground="{Binding TextBrush}" Margin="12"
VerticalAlignment="Center"/>
</Border>
<!-- The modified date -->
<TextBlock Foreground="{StaticResource PhoneSubtleBrush}"
Text="{Binding Modified, Converter={StaticResource DateConverter}}"
Margin="24,0,0,12"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- The user control -->
<local:LoginControl x:Name="LoginControl" Grid.Row="1"
Closed="LoginControl_Closed"/>
</Grid>
</phone:PhoneApplicationPage>
Notes:
→ The ampersand in the app’s title is XML encoded to avoid a XAML parsing error.
→ The LoginControl
user control is used as a part of this page, rather than as a separate login page, to ensure a sensible navigation flow. When the user opens the app, logs in, and then sees the data on the main page, pressing the hardware Back button should exit the app, not go back to a login page!
→ LoginControl
doesn’t protect the data simply by visually covering it up; you’ll see in the code-behind that it isn’t populated until after login. And there’s no way for the app to show the data before login because the correct password is needed to properly decrypt the stored notes.
→ The list box’s item template binds to several properties of each note. (The Note
class used to represent each one is shown later in this chapter.) The binding to the Modified
property uses something called a value converter to change the resultant display. Value converters are discussed next.
In data binding, value converters can morph a source value into a completely different target value. They enable you to plug in custom logic without giving up the benefits of data binding.
Value converters are often used to reconcile a source and target that are different data types. For example, you could change the background or foreground color of an element based on the value of some nonbrush data source, à la conditional formatting in Microsoft Excel. As another example, the toggle switch in the Silverlight for Windows Phone Toolkit leverages a value converter called OnOffConverter
that converts the nullable Boolean IsChecked
value to an “On” or “Off” string used as its default content.
In Passwords & Secrets, we want to slightly customize the display of each note’s Modified
property. Modified
is of type DateTimeOffset
, so without a value converter applied, it would appear as follows:
12/11/2012 10:18:49 PM -08:00
The -08:00 represents the time zone. It is expressed as an offset from Coordinated Universal Time (UTC).
Our custom value converter strips off the time zone information and the seconds, as that’s more information than we need. It produces a result like the following:
12/11/2010 10:18 PM
Even if Modified
were a DateTime
instead of a DateTimeOffset
, the value converter would still be useful for stripping the seconds value out of the string.
To create a value converter, you must write a class that implements an IValueConverter
interface in the System.Windows.Data
namespace. This interface has two simple methods—Convert
, which is passed the source instance that must be converted to the target instance, and ConvertBack
, which does the opposite. Listing 21.7 contains the implementation of the DateConverter
value converter used in Listing 21.6.
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace WindowsPhoneApp
{
public class DateConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
DateTimeOffset date = (DateTimeOffset)value;
// Return a custom format
return date.LocalDateTime.ToShortDateString() + " "
+ date.LocalDateTime.ToShortTimeString();
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
{
return DependencyProperty.UnsetValue;
}
}
}
The Convert
method is called every time the source value changes. It’s given the DateTimeOffset
value and returns a string with the date and time in a short format. The ConvertBack
method is not needed, as it is only invoked in two-way data binding. Therefore, it returns a dummy value.
Value converters can be applied to any data binding with its Converter
parameter. This was done in Listing 21.6 as follows:
<!-- The modified date -->
<TextBlock Foreground="{StaticResource PhoneSubtleBrush}"
Text="{Binding Modified, Converter={StaticResource DateConverter}}"
Margin="24,0,0,12"/>
Setting this via StaticResource
syntax requires an instance of the converter class to be defined in an appropriate resource dictionary. Listing 21.6 added an instance with the DateConverter
key to the page’s resource dictionary:
<phone:PhoneApplicationPage.Resources>
<local:DateConverter x:Key="DateConverter"/>
</phone:PhoneApplicationPage.Resources>
The code-behind for the main page is shown in Listing 21.8.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
IApplicationBarMenuItem passwordMenuItem;
public MainPage()
{
InitializeComponent();
this.passwordMenuItem = this.ApplicationBar.MenuItems[0]
as IApplicationBarMenuItem;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// The password menu item is "show password hint" when not logged in,
// or "change password" when logged in
if (CurrentContext.IsLoggedIn)
{
this.passwordMenuItem.Text = "change password";
// This is only needed when reactivating app and navigating back to this
// page from the details page, because going back can instantiate
// this page in a logged-in state
this.LoginControl.Close();
}
else
{
this.passwordMenuItem.Text = "show password hint";
}
// Clear the selection so selecting the same item twice in a row will
// still raise the SelectionChanged event
Settings.CurrentNoteIndex.Value = -1;
this.ListBox.SelectedIndex = -1;
if (Settings.NotesList.Value.Count == 0)
this.NoItemsTextBlock.Visibility = Visibility.Visible;
else
this.NoItemsTextBlock.Visibility = Visibility.Collapsed;
}
void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ListBox.SelectedIndex >= 0)
{
// Navigate to the details page for the selected item
Settings.CurrentNoteIndex.Value = ListBox.SelectedIndex;
this.NavigationService.Navigate(new Uri("/DetailsPage.xaml",
UriKind.Relative));
}
}
void LoginControl_Closed(object sender, EventArgs e)
{
// Now that we're logged-in, add the "new" button to the application bar
ApplicationBarIconButton newButton = new ApplicationBarIconButton
{
Text = "new",
IconUri = new Uri("/Shared/Images/appbar.add.png", UriKind.Relative)
};
newButton.Click += NewButton_Click;
this.ApplicationBar.Buttons.Add(newButton);
// The password menu item is "show password hint" when not logged in,
// or "change password" when logged in
this.passwordMenuItem.Text = "change password";
// Now bind the notes list as the data source for the list box,
// because its contents can be decrypted
this.DataContext = Settings.NotesList.Value;
}
// Application bar handlers
void NewButton_Click(object sender, EventArgs e)
{
// Create a new note and add it to the top of the list
Note note = new Note();
note.Modified = DateTimeOffset.Now;
note.ScreenColor = Settings.ScreenColor.Value;
note.TextColor = Settings.TextColor.Value;
note.TextSize = Settings.TextSize.Value;
Settings.NotesList.Value.Insert(0, note);
// "Select" the new note
Settings.CurrentNoteIndex.Value = 0;
// Navigate to the details page for the newly created note
this.NavigationService.Navigate(new Uri("/DetailsPage.xaml",
UriKind.Relative));
}
void PasswordMenuItem_Click(object sender, EventArgs e)
{
if (CurrentContext.IsLoggedIn)
{
// Change password
this.NavigationService.Navigate(new Uri("/ChangePasswordPage.xaml",
UriKind.Relative));
}
else
{
// Show password hint
if (Settings.PasswordHint.Value == null ||
Settings.PasswordHint.Value.Trim().Length == 0)
{
MessageBox.Show("Sorry, but there is no hint!", "Password hint",
MessageBoxButton.OK);
}
else
{
MessageBox.Show(Settings.PasswordHint.Value, "Password hint",
MessageBoxButton.OK);
}
}
}
void InstructionsMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri("/InstructionsPage.xaml",
UriKind.Relative));
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(
new Uri("/Shared/About/AboutPage.xaml?appName=Passwords %26 Secrets",
UriKind.Relative));
}
}
}
Notes:
→ The first menu item on the application bar, shown expanded in Figure 21.3, reveals the password hint when the user is logged out and navigates to the change password page when the user is logged in.
→ As seen earlier, the NotesList
collection used as the data context for the list box is not just any collection (like List<Note>
); it’s an observable collection:
public static readonly Setting<ObservableCollection<Note>> NotesList =
new Setting<ObservableCollection<Note>>("NotesList",
new ObservableCollection<Note>());
Observable collections raise a CollectionChanged
event whenever any changes occur, such as items being added or removed. Data binding automatically leverages this event to keep the target (the list box, in this page) up-to-date at all times. Thanks to this, Listing 21.8 simply sets the page’s data context to the list and the rest takes care of itself.
INotifyPropertyChanged
InterfaceAlthough the observable collection takes care off additions and deletions being reflected in the list box, each Note
item must provide notifications to ensure that item-specific property changes are reflected in the databound list box. Note
does this by implementing INotifyPropertyChanged
, as shown in Listing 21.9.
using System;
using System.ComponentModel;
using System.Windows.Media;
namespace WindowsPhoneApp
{
public class Note : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
// A helper method used by the properties
void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
string encryptedContent;
public string EncryptedContent
{
get { return this.encryptedContent; }
set { this.encryptedContent = value;
OnPropertyChanged("EncryptedContent"); OnPropertyChanged("Title"); }
}
DateTimeOffset modified;
public DateTimeOffset Modified
{
get { return this.modified; }
set { this.modified = value; OnPropertyChanged("Modified"); }
}
int textSize;
public int TextSize
{
get { return this.textSize; }
set { this.textSize = value; OnPropertyChanged("TextSize"); }
}
Color screenColor;
public Color ScreenColor
{
get { return this.screenColor; }
set { this.screenColor = value;
OnPropertyChanged("ScreenColor"); OnPropertyChanged("ScreenBrush"); }
}
Color textColor;
public Color TextColor
{
get { return this.textColor; }
set { this.textColor = value;
OnPropertyChanged("TextColor"); OnPropertyChanged("TextBrush"); }
}
// Three readonly properties whose value is computed from other properties:
public Brush ScreenBrush
{
get { return new SolidColorBrush(this.ScreenColor); }
}
public Brush TextBrush
{
get { return new SolidColorBrush(this.TextColor); }
}
public string Title
{
get
{
// Grab the note's content
string title =
Crypto.Decrypt(this.EncryptedContent, CurrentContext.Password) ?? "";
// Don't include more than the first 100 characters, which should be long
// enough, even in landscape with a small font
if (title.Length > 100)
title = title.Substring(0, 100);
// Fold the remaining content into a single line. We can't use
// Environment.NewLine because it's
, whereas newlines inserted from
// a text box are just
return title.Replace('
', ' ');
}
}
}
}
Notes:
→ INotifyPropertyChanged
has a single member—a PropertyChanged
event. If the implementer raises this event at the appropriate time with the name of each property that has changed, data binding takes care of refreshing any targets.
→ The raising of the PropertyChanged
event is handled by the OnPropertyChanged
helper method. The event handler field is assigned to a handler
variable to avoid a potential bug. Without this, if a different thread removed the last handler between the time that the current thread checked for null
and performed the invocation, a NullReferenceException
would be thrown. (The event handler field becomes null
when no more listeners are attached.)
→ Notice that some properties, when changed, raise the PropertyChanged
event for an additional property. For example, when EncryptedContent
is set to a new value, a PropertyChanged
event is raised for the readonly Title
property. This is done because the value of Title
is based on the value of EncryptedContent
, so a change to EncryptedContent
may change Title
.
The details page, shown in Figure 21.4, appears when the user taps a note in the list box on the main page. This page displays the entire contents of the note and enables the user to edit it, delete it, or email its contents. It also provides access to a per-note settings page that gives control over the note’s colors and text size. Listing 21.10 contains this page’s XAML.
<phone:PhoneApplicationPage
x:Class="WindowsPhoneApp.DetailsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:local="clr-namespace:WindowsPhoneApp"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
SupportedOrientations="PortraitOrLandscape" shell:SystemTray.IsVisible="True">
<!-- The application bar, with three buttons -->
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar IsVisible="False">
<shell:ApplicationBarIconButton Text="delete"
IconUri="/Shared/Images/appbar.delete.png"
Click="DeleteButton_Click"/>
<shell:ApplicationBarIconButton Text="email"
IconUri="/Shared/Images/appbar.email.png"
Click="EmailButton_Click"/>
<shell:ApplicationBarIconButton Text="settings"
IconUri="/Shared/Images/appbar.settings.png"
Click="SettingsButton_Click"/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<phone:PhoneApplicationPage.Resources>
<!-- A copy of the text box default style with its border removed and
background applied differently. Compare with the style in Program Files
Microsoft SDKsWindows Phonev7.0DesignSystem.Windows.xaml -->
...
</phone:PhoneApplicationPage.Resources>
<ScrollViewer>
<Grid>
<!-- The full-screen text box -->
<TextBox x:Name="TextBox" InputScope="Text"
Style="{StaticResource PhoneTextBox}"
AcceptsReturn="True" TextWrapping="Wrap"
GotFocus="TextBox_GotFocus" LostFocus="TextBox_LostFocus"/>
<!-- The user control -->
<local:LoginControl x:Name="LoginControl" Closed="LoginControl_Closed"/>
</Grid>
</ScrollViewer>
</phone:PhoneApplicationPage>
The text box that basically occupies the whole screen is given a custom style that removes its border and ensures the desired background color remains visible whether the text box has focus. The style was created by copying the default style from %ProgramFiles%Microsoft SDKsWindows Phonev7.0DesignSystem.Windows.xaml
then making a few tweaks.
Listing 21.11 contains the code-behind for this page.
using System;
using System.Windows;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Tasks;
namespace WindowsPhoneApp
{
public partial class DetailsPage : PhoneApplicationPage
{
bool navigatingFrom;
string initialText = "";
public DetailsPage()
{
InitializeComponent();
this.Loaded += DetailsPage_Loaded;
}
void DetailsPage_Loaded(object sender, RoutedEventArgs e)
{
if (CurrentContext.IsLoggedIn)
{
// Automatically show the keyboard for new notes.
// This also gets called when navigating away, hence the extra check
// to make sure we're only doing this when navigating to the page
if (this.TextBox.Text.Length == 0 && !this.navigatingFrom)
this.TextBox.Focus();
}
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
this.navigatingFrom = true;
base.OnNavigatedFrom(e);
if (this.initialText != this.TextBox.Text)
{
// Automatically save the new content
Note n = Settings.NotesList.Value[Settings.CurrentNoteIndex.Value];
n.EncryptedContent =
Crypto.Encrypt(this.TextBox.Text, CurrentContext.Password) ?? "";
n.Modified = DateTimeOffset.Now;
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (CurrentContext.IsLoggedIn)
this.LoginControl.Close();
}
void TextBox_GotFocus(object sender, RoutedEventArgs e)
{
this.ApplicationBar.IsVisible = false;
}
void TextBox_LostFocus(object sender, RoutedEventArgs e)
{
this.ApplicationBar.IsVisible = true;
}
void LoginControl_Closed(object sender, EventArgs e)
{
this.ApplicationBar.IsVisible = true;
// Show the note's contents
Note n = Settings.NotesList.Value[Settings.CurrentNoteIndex.Value];
if (n != null)
{
this.TextBox.Background = n.ScreenBrush;
this.TextBox.Foreground = n.TextBrush;
this.TextBox.FontSize = n.TextSize;
this.initialText = this.TextBox.Text =
Crypto.Decrypt(n.EncryptedContent, CurrentContext.Password) ?? "";
}
}
// Application bar handlers:
void DeleteButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show("Are you sure you want to delete this note?",
"Delete note?", MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
Settings.NotesList.Value.Remove(
Settings.NotesList.Value[Settings.CurrentNoteIndex.Value]);
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
}
void EmailButton_Click(object sender, EventArgs e)
{
EmailComposeTask launcher = new EmailComposeTask();
launcher.Body = this.TextBox.Text;
launcher.Subject = "Note";
launcher.Show();
}
void SettingsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri("/SettingsPage.xaml",
UriKind.Relative));
}
}
}
Notes:
→ This page uses a navigatingFrom
flag to check whether the page is in the process of navigating away. That’s because the Loaded
event gets raised a second time after OnNavigatedFrom
, and applying focus to the text box at this time could cause an unwanted flicker from the on-screen keyboard briefly appearing.
→ The code for the settings page linked to this page is shown in the next chapter, because it is identical to the one used by this app!
3.144.243.184