Reader Level:
ARTICLE

Programming with App Bar and Jot Control in Windows Phone 7

Posted by Charles Petzold Articles | Windows Phone December 07, 2010
The ApplicationBar serves the same role as a menu or toolbar that you might find in a conventional Windows program and Jot displays finger input using a class named InkPresenter, which originated with tablet interfaces.
  • 0
  • 0
  • 6198
Download Files:
 

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:

te1.gif

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:

t2.gif

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:

t3.gif

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:

        protected
override 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.

COMMENT USING

Trending up