This chapter
is taken from book "Programming Windows Phone 7" by Charles Petzold published by
Microsoft press.
http://www.charlespetzold.com/phone/index.html
The basic program commands like ApplicationBar it is
commonly referred to as the app bar might best be implemented in a mechanism
developed specifically for the phone and which is intended to provide a
consistent user experience for phone users.
ApplicationBar Icons
The ApplicationBar serves the same role as a menu or toolbar
that you might find in a conventional Windows program, ApplicationBar and
related classes are defined in the
Microsoft.Phone.Shell
namespace. These
classes derive from Object and exist entirely apart from the whole
DependencyObject, UIElement, and FrameworkElement class hierarchy of
conventional Silverlight programming.
The ApplicationBar is not part of standard Silverlight, so an
XML namespace declaration needs to associate the XML "shell" namespace with the
.NET namespace Microsoft.Phone.Shell. The standard MainPage.xaml file provides
this for you already:
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
ApplicationBar has a property named Buttons which is the
content property for the class. The Buttons collection can contain no more than
four ApplicationBarIconButton objects. The IconUri and Text fields are required!
The text description should be short; it is converted to lower-case for display
purposes.
The final MoviePlayer project has an
ApplicationBar defined like this:
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton
x:Name="appbarRewindButton"
IconUri="Images/appbar.transport.rew.rest.png"
Text="rewind"
IsEnabled="False"
Click="OnAppbarRewindClick"
/>
<shell:ApplicationBarIconButton
x:Name="appbarPlayButton"
IconUri="Images/appbar.transport.play.rest.png"
Text="play"
IsEnabled="False"
Click="OnAppbarPlayClick"
/>
<shell:ApplicationBarIconButton
x:Name="appbarPauseButton"
IconUri="Images/appbar.transport.pause.rest.png"
Text="pause"
IsEnabled="False"
Click="OnAppbarPauseClick"
/>
<shell:ApplicationBarIconButton
x:Name="appbarEndButton"
IconUri="Images/appbar.transport.ff.rest.png"
Text="to
end"
IsEnabled="False"
Click="OnAppbarEndClick"
/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
Yes, I have assigned
x:Name
attributes to all the buttons, but you'll see shortly that I've also reassigned
them in code.
The content grid contains the
MediaElement to
play the movie and two TextBlock elements for some status and error messages:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<MediaElement
Name="mediaElement"
Source="http://www.charlespetzold.com/Media/Walrus.wmv"
AutoPlay="False"
MediaOpened="OnMediaElementMediaOpened"
MediaFailed="OnMediaElementMediaFailed"
CurrentStateChanged="OnMediaElementCurrentStateChanged"
/>
<TextBlock
Name="statusText"
HorizontalAlignment="Left"
VerticalAlignment="Bottom" />
<TextBlock
Name="errorText"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
TextWrapping="Wrap"
/>
</Grid>
The constructor of the MainPage assigns x:Name attributes to the appropriate ApplicationBarIconButton so they can be conveniently referenced in the rest of the classand the four handlers for the ApplicationBar buttons have just one line of code each, and finaly the messy part of a movie-playing program involves the enabling and disabling of the buttons. Because the primary purpose of this program is to demonstrate the use of the ApplicationBar, I've taken a very simple approach here: The Rewind and End buttons are enabled when the media is opened, and the Play and Pause buttons are enabled based on the CurrentState property of the MediaElement:
namespace MoviePlayer
{
public partialclass MainPage : PhoneApplicationPage
{
// Constructor
public MainPage()
{
InitializeComponent();
// Re-assign names already in the XAML file
appbarRewindButton = this.ApplicationBar.Buttons[0]as ApplicationBarIconButton;
appbarPlayButton = this.ApplicationBar.Buttons[1]as ApplicationBarIconButton;
appbarPauseButton = this.ApplicationBar.Buttons[2]as ApplicationBarIconButton;
appbarEndButton = this.ApplicationBar.Buttons[3]as ApplicationBarIconButton;
}
// ApplicationBar buttons
void OnAppbarRewindClick(object sender, EventArgs args)
{
mediaElement.Position = TimeSpan.Zero;
}
void OnAppbarPlayClick(object sender, EventArgs args)
{
mediaElement.Play();
}
void OnAppbarPauseClick(object sender, EventArgs args)
{
mediaElement.Pause();
}
void OnAppbarEndClick(object sender, EventArgs args)
{
mediaElement.Position = mediaElement.NaturalDuration.TimeSpan;
}
// MediaElement events
void OnMediaElementMediaFailed(object sender,ExceptionRoutedEventArgs args)
{
errorText.Text = args.ErrorException.Message;
}
void OnMediaElementMediaOpened(object sender, RoutedEventArgs args)
{
appbarRewindButton.IsEnabled = true;
appbarEndButton.IsEnabled = true;
}
void OnMediaElementCurrentStateChanged(object sender, RoutedEventArgs args)
{
statusText.Text = mediaElement.CurrentState.ToString();
if (mediaElement.CurrentState ==MediaElementState.Stopped ||
mediaElement.CurrentState == MediaElementState.Paused)
{
appbarPlayButton.IsEnabled = true;
appbarPauseButton.IsEnabled = false;
}
else if (mediaElement.CurrentState == MediaElementState.Playing)
{
appbarPlayButton.IsEnabled = false;
appbarPauseButton.IsEnabled = true;
}
}
}
}
Now it looks like this:
Jot and Application
Settings
Jot displays finger input using a class named InkPresenter,
which originated with tablet interfaces. InkPresenter derives from the Canvas
panel, which means you could use the Children property of
InkPresenter to design a background image (a yellow
legal pad, for example). Or you can ignore the Canvas
part of InkPresenter.
The application settings for Jot are encapsulated in a class specifically for
that purpose called JotAppSettings. An instance of JotAppSettings is serialized and saved in isolated storage. The class also
contains methods to save and load the settings. The project needs a reference to
the System.Xml.Serialization library, and JotAppSettings needs several non-standard using
directives for
System.Collection.Generic, System.IO,
System.IO.IsolatedStorage,
and
System.Xml.Serialization.
Here are the public properties of
JotAppSettings that constitute application settings and using the IsolatedStorageSettings class to save these items, but I couldn't get it to work,
so I switched to the regular isolated storage facility. Here's the methods:
public List<StrokeCollection> StrokeCollections { get; set; }
publicint PageNumber { set;get; }
publicColor Foreground { set;get; }
publicColor Background { set;get; }
publicint StrokeWidth { set;get; }
public void Save()
{
IsolatedStorageFile iso = IsolatedStorageFile.GetUserStoreForApplication();
IsolatedStorageFileStream stream = iso.CreateFile("settings.xml");
StreamWriter writer = newStreamWriter(stream);
XmlSerializer ser = newXmlSerializer(typeof(JotAppSettings));
ser.Serialize(writer, this);
writer.Close();
iso.Dispose();
}
The Load method is static because it must create an instance of JotAppSettings by deserializing the file in isolated storage. If that file doesn't exist—which means the program is being run for the first time-then it simply creates a new instance.
public staticJotAppSettings Load()
{
JotAppSettings settings;
IsolatedStorageFile iso = IsolatedStorageFile.GetUserStoreForApplication();
if (iso.FileExists("settings.xml"))
{
IsolatedStorageFileStream stream = iso.OpenFile("settings.xml",FileMode.Open);
StreamReader reader = newStreamReader(stream);
XmlSerializer ser = newXmlSerializer(typeof(JotAppSettings));
settings = ser.Deserialize(reader)asJotAppSettings;
reader.Close();
}
else
{
// Create and initialize new JotAppSettings object
settings = newJotAppSettings();
settings.StrokeCollections = newList<StrokeCollection>();
settings.StrokeCollections.Add(newStrokeCollection());
}
iso.Dispose();
return settings;
}
Jot and Touch
The content area of Jot is tiny but significant:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<InkPresenter
Name="inkPresenter"
/>
</Grid>
As the name suggests, the InkPresenter renders virtual ink that comes from stylus or touch input. The InkPresenter doesn't collect that ink on its own. That's your responsibility. (And Silverlight has no built-in handwriting recognition, although there's nothing to prevent you from adding your own.)
The code-behind file requires a
using directive for the
System.Windows.Ink namespace and defines just two
private fields and the OnTouchFrameReported
handler:
public
partial class
MainPage :
PhoneApplicationPage
{
JotAppSettings appSettings = (Application.Current
as App).AppSettings;
Dictionary<int,
Stroke> activeStrokes =
new Dictionary<int,
Stroke>();
public MainPage()
{
InitializeComponent();
inkPresenter.Strokes =
appSettings.StrokeCollections[appSettings.PageNumber];
inkPresenter.Background = new
SolidColorBrush(appSettings.Background);
// Re-assign ApplicationBar button names
appbarLastButton = this.ApplicationBar.Buttons[1]
as
ApplicationBarIconButton;
appbarNextButton = this.ApplicationBar.Buttons[2]
as
ApplicationBarIconButton;
appbarDeleteButton = this.ApplicationBar.Buttons[3]
as
ApplicationBarIconButton;
TitleAndAppbarUpdate();
Touch.FrameReported +=
OnTouchFrameReported;
}
void OnTouchFrameReported(object
sender, TouchFrameEventArgs args)
{
TouchPoint primaryTouchPoint =
args.GetPrimaryTouchPoint(null);
if (primaryTouchPoint !=
null && primaryTouchPoint.Action ==
TouchAction.Down)
args.SuspendMousePromotionUntilTouchUp();
TouchPointCollection touchPoints
= args.GetTouchPoints(inkPresenter);
foreach (TouchPoint
touchPoint in touchPoints)
{
Point pt =
touchPoint.Position;
int id =
touchPoint.TouchDevice.Id;
switch (touchPoint.Action)
{
case
TouchAction.Down:
Stroke stroke = new
Stroke();
stroke.DrawingAttributes.Color = appSettings.Foreground;
stroke.DrawingAttributes.Height =
appSettings.StrokeWidth;
stroke.DrawingAttributes.Width = appSettings.StrokeWidth;
stroke.StylusPoints.Add(new
StylusPoint(pt.X, pt.Y));
inkPresenter.Strokes.Add(stroke);
activeStrokes.Add(id, stroke);
break;
case
TouchAction.Move:
activeStrokes[id].StylusPoints.Add(new
StylusPoint(pt.X, pt.Y));
break;
case
TouchAction.Up:
activeStrokes[id].StylusPoints.Add(new
StylusPoint(pt.X, pt.Y));
activeStrokes.Remove(id);
TitleAndAppbarUpdate();
break;
}
}
}
Jot and the ApplicationBar
The ApplicatonBar in Jot defines four buttons: for adding a new page, going
to the previous page, going to the next page, and deleting the current page. (If
the current page is the only page, then only the strokes are deleted from the
page.) Each button has its own Click handler:
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton
x:Name="appbarAddButton"
IconUri="/Images/appbar.add.rest.png"
Text="add
page"
Click="OnAppbarAddClick"
/>
<shell:ApplicationBarIconButton
x:Name="appbarLastButton"
IconUri="/Images/appbar.back.rest.png"
Text="last
page"
Click="OnAppbarLastClick"
/>
<shell:ApplicationBarIconButton
x:Name="appbarNextButton"
IconUri="/Images/appbar.next.rest.png"
Text="next
page"
Click="OnAppbarNextClick"
/>
<shell:ApplicationBarIconButton
x:Name="appbarDeleteButton"
IconUri="/Images/appbar.delete.rest.png"
Text="delete
page"
Click="OnAppbarDeleteClick"
/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem
Text="swap colors"
Click="OnAppbarSwapColorsClick"
/>
<shell:ApplicationBarMenuItem
Text="light stroke width"
Click="OnAppbarSetStrokeWidthClick"
/>
<shell:ApplicationBarMenuItem
Text="medium stroke
width"
Click="OnAppbarSetStrokeWidthClick"
/>
<shell:ApplicationBarMenuItem
Text="heavy stroke width"
Click="OnAppbarSetStrokeWidthClick"
/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
A menu is also included with a collection of ApplicationBarMenuItem objects
in the MenuItems property element. The menu items are displayed when you press
the ellipsis on the ApplicationBar. They consist solely of short text strings in
lower-case. (You should keep menu items to five or fewer, and keep the text to a
maximum of 20 characters or so.) The first menu item (to swap the colors) has
its own Click handler; the other three share a Click
handler.
Here are the
Click
handlers for the four buttons:
void
OnAppbarAddClick(object sender,
EventArgs args)
{
StrokeCollection strokes =
new StrokeCollection();
appSettings.PageNumber += 1;
appSettings.StrokeCollections.Insert(appSettings.PageNumber,
strokes);
inkPresenter.Strokes = strokes;
TitleAndAppbarUpdate();
}
void OnAppbarLastClick(object
sender, EventArgs args)
{
appSettings.PageNumber -= 1;
inkPresenter.Strokes =
appSettings.StrokeCollections[appSettings.PageNumber];
TitleAndAppbarUpdate();
}
void OnAppbarNextClick(object
sender, EventArgs args)
{
appSettings.PageNumber += 1;
inkPresenter.Strokes =
appSettings.StrokeCollections[appSettings.PageNumber];
TitleAndAppbarUpdate();
}
void OnAppbarDeleteClick(object
sender, EventArgs args)
{
MessageBoxResult result =
MessageBox.Show("Delete
this page?", "Jot",
MessageBoxButton.OKCancel);
if (result ==
MessageBoxResult.OK)
{
if (appSettings.StrokeCollections.Count
== 1)
{
appSettings.StrokeCollections[0].Clear();
}
else
{
appSettings.StrokeCollections.RemoveAt(appSettings.PageNumber);
if (appSettings.PageNumber
== appSettings.StrokeCollections.Count)
appSettings.PageNumber -= 1;
inkPresenter.Strokes =
appSettings.StrokeCollections[appSettings.PageNumber];
}
TitleAndAppbarUpdate();
}
}
The message box is displayed at the top of the screen
and disables the rest of the application until you make it go away:
RangeBase and
Slider
The RangeBase class defines Minimum, Maximum, SmallChange, and LargeChange
properties to define the parameters of scrolling, plus a Value property for the
user's selection and a ValueChanged event that signals when Value has changed.
(Notice that ProgressBar also derives from RangeBase, but the Value
property is always controlled
programmatically rather than being set by the user.)
I'm going to focus on the
Slider here because the version in Windows Phone 7
seems a little more tailored to the phone than the ScrollBar. The goal is to use
three Slider controls to create a program called ColorScroll that looks like
this:
That larger Grid with the two
cells is the familiar Grid named ContentPanel. Whether those two cells are two
rows or two columns is determined by the code-behind file based on the current
Orientation property.
The XAML file contains a
Resources collection with Style definitions for
both TextBlock and Slider:
<phone:PhoneApplicationPage.Resources>
<Style
x:Key="textStyle"
TargetType="TextBlock">
<Setter
Property="HorizontalAlignment"
Value="Center"
/>
</Style>
<Style
x:Key="sliderStyle"
TargetType="Slider">
<Setter
Property="Minimum"
Value="0"
/>
<Setter
Property="Maximum"
Value="255"
/>
<Setter
Property="Orientation"
Value="Vertical"
/>
</Style>
</phone:PhoneApplicationPage.Resources>
By default, the top of a vertical Slider is associated with the Maximum
value. That's OK for this program but you can change it by setting the
IsDirectionReversed property to true.
Here's the whole content panel:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<Grid.RowDefinitions>
<RowDefinition
Height="*" />
<RowDefinition
Height="*" />
</Grid.RowDefinitions>
<Rectangle
Name="rect"
Grid.Row="0"
Grid.Column="0"
/>
<Grid
Name="controlGrid"
Grid.Row="1"
Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition
Height="Auto" />
<RowDefinition
Height="*" />
<RowDefinition
Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="*" />
<ColumnDefinition
Width="*" />
<ColumnDefinition
Width="*" />
</Grid.ColumnDefinitions>
<!-- Red column -->
<TextBlock
Grid.Column="0"
Grid.Row="0"
Text="Red"
Foreground="Red"
Style="{StaticResource
textStyle}" />
<Slider
Name="redSlider"
Grid.Column="0"
Grid.Row="1"
Foreground="Red"
Style="{StaticResource
sliderStyle}"
ValueChanged="OnSliderValueChanged"
/>
<TextBlock
Name="redText"
Grid.Column="0"
Grid.Row="2"
Text="0"
Foreground="Red"
Style="{StaticResource
textStyle}" />
<!-- Green column -->
<TextBlock
Grid.Column="1"
Grid.Row="0"
Text="Green"
Foreground="Green"
Style="{StaticResource
textStyle}" />
<Slider
Name="greenSlider"
Grid.Column="1"
Grid.Row="1"
Foreground="Green"
Style="{StaticResource
sliderStyle}"
ValueChanged="OnSliderValueChanged"
/>
<TextBlock
Name="greenText"
Grid.Column="1"
Grid.Row="2"
Text="0"
Foreground="Green"
Style="{StaticResource
textStyle}" />
<!-- Blue column -->
<TextBlock
Grid.Column="2"
Grid.Row="0"
Text="Blue"
Foreground="Blue"
Style="{StaticResource
textStyle}" />
<Slider
Name="blueSlider"
Grid.Column="2"
Grid.Row="1"
Foreground="Blue"
Style="{StaticResource
sliderStyle}"
ValueChanged="OnSliderValueChanged"
/>
<TextBlock
Name="blueText"
Grid.Column="2"
Grid.Row="2"
Text="0"
Foreground="Blue"
Style="{StaticResource
textStyle}" />
</Grid>
All the Slider controls have their ValueChanged
events set to the same handler. This handler really takes an easy way out by not
bothering to determine which
Slider
actually raised the event:
void OnSliderValueChanged(object sender, RoutedPropertyChangedEventArgs<double> args)
{
Color clr = Color.FromArgb(255, (byte)redSlider.Value,
(byte)greenSlider.Value,
(byte)blueSlider.Value);
rect.Fill = newSolidColorBrush(clr);
redText.Text = clr.R.ToString("X2");
greenText.Text = clr.G.ToString("X2");
blueText.Text = clr.B.ToString("X2");
}
You can check for portrait or landscape by performing a bitwise OR operation between the Orientation property and the Portrait or Landscape members, and then checking for a nonzero result. It makes the code just a little simpler:
protectedoverride void OnOrientationChanged(OrientationChangedEventArgs args)
{
ContentPanel.RowDefinitions.Clear();
ContentPanel.ColumnDefinitions.Clear();
// Landscape
if ((args.Orientation &PageOrientation.Landscape) != 0)
{
ColumnDefinition coldef =new ColumnDefinition();
coldef.Width = newGridLength(1, GridUnitType.Star);
ContentPanel.ColumnDefinitions.Add(coldef);
coldef = newColumnDefinition();
coldef.Width = newGridLength(1, GridUnitType.Star);
ContentPanel.ColumnDefinitions.Add(coldef);
Grid.SetRow(controlGrid, 0);
Grid.SetColumn(controlGrid, 1);
}
// Portrait
else
{
RowDefinition rowdef =new RowDefinition();
rowdef.Height = newGridLength(1, GridUnitType.Star);
ContentPanel.RowDefinitions.Add(rowdef);
rowdef = newRowDefinition();
rowdef.Height = newGridLength(1, GridUnitType.Star);
ContentPanel.RowDefinitions.Add(rowdef);
Grid.SetRow(controlGrid, 1);
Grid.SetColumn(controlGrid, 0);
}
base.OnOrientationChanged(args);
}
The ContentPanel object needs to be switched between two rows for portrait mode and two columns for landscape mode, so it creates the GridDefinition and ColumnDefinition
objects for the new orientation.