O'Reilly logo

Programming .NET 3.5 by Jesse Liberty, Alex Horovitz

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. Applying WPF: Building a Biz App

The previous chapter introduced various aspects of working with WPF. In this chapter you’re going to build a larger desktop application that has two significant “pages”: on the first page (shown in Figure 4-1), users will be able to choose among photographs, crop them, and then add prints and related items to the shopping cart; on the second page (shown in Figure 4-2), users will pay for the items in their carts using a credit card.

Page 1
Figure 4-1. Page 1
Page 2
Figure 4-2. Page 2

Breaking the Application into Pieces

This application clearly lends itself to being developed in a number of pieces. The first division is between the first page, in which the user adds items to the shopping cart, and the second page, in which the user pays for those items.

Page 1, in turn, can easily be divided into its component parts: the photo slider you developed in the last chapter, the central photo display, the shopping cart area, and the surrounding areas.

To begin, create a new WPF Application called PhotoCooperative. You will use this project as a container for the code you create and evaluate along the way.

Adorners

The spec for this project states that the user can drag a cropping rectangle within any photograph and then click the Crop button to crop the photo, as shown in Figure 4-3.

To accomplish this, you’ll need to be able to create and display a “rubberband” on mouse down and mouse drag, and leave it in place on mouse up (activating the Crop button)—and you’ll need to be able to “crop” the photo to its new rectangle.

You’ll implement the rubberband as an adorner. Adorners are, essentially, elements that are rendered “on top of” existing elements (or collections of elements). You can think of WPF as having what amounts to an acetate layer of adorners that can be laid on top of adorned elements, with the adorners positioned relative to the elements that are being adorned.

Cropping a photo
Figure 4-3. Cropping a photo

Adorners are often used to create element-manipulation handles (like rotation handles or resizers) or visual feedback indications. A rubberband for cropping is an excellent example. In fact, Microsoft offers sample code that you can “borrow” and adapt for the purposes of this application, as shown in Example 4-1.

Example 4-1. The rubberband adorner
using System;
using System.IO;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Documents;

namespace PhotoCooperative
{
   public class RubberbandAdorner : Adorner
   {
      public Window1 Window { set; get; }
      private RectangleGeometry geometry;
      public System.Windows.Shapes.Path Rubberband { get; set; }
      private UIElement adornedElement;
      private Rect selectRect;
      public Rect SelectRect { get { return selectRect; } }
      protected override int VisualChildrenCount { get { return 1; } }
      private Point anchorPoint;

      public RubberbandAdorner(UIElement adornedElement) : base(adornedElement)
      {
         this.adornedElement = adornedElement;
         selectRect = new Rect();
         geometry = new RectangleGeometry();
         Rubberband = new System.Windows.Shapes.Path();
         Rubberband.Data = geometry;
         Rubberband.StrokeThickness = 2;
         Rubberband.Stroke = Brushes.Yellow;
         Rubberband.Opacity = .6;
         Rubberband.Visibility = Visibility.Hidden;
         AddVisualChild(Rubberband);
         MouseMove += new MouseEventHandler(DrawSelection);
         MouseUp += new MouseButtonEventHandler(EndSelection);
      }

      protected override Size ArrangeOverride(Size size)
      {
         Size finalSize = base.ArrangeOverride(size);
         ((UIElement)GetVisualChild(0)).Arrange(new Rect(new Point(), finalSize));
         return finalSize;
      }

      public void StartSelection(Point anchorPoint)
      {
         this.anchorPoint = anchorPoint;
         selectRect.Size = new Size(10, 10);
         selectRect.Location = anchorPoint;
         geometry.Rect = selectRect;
         if (Visibility.Visible != Rubberband.Visibility)
             Rubberband.Visibility = Visibility.Visible;
      }

      private void DrawSelection(object sender, MouseEventArgs e)
      {
         if (e.LeftButton == MouseButtonState.Pressed)
         {
            Point mousePosition = e.GetPosition(adornedElement);
            if (mousePosition.X < anchorPoint.X)
            {
               selectRect.X = mousePosition.X;
            }
            else
            {
               selectRect.X = anchorPoint.X;
            }
            if (mousePosition.Y < anchorPoint.Y)
            {
               selectRect.Y = mousePosition.Y;
            }
            else
            {
               selectRect.Y = anchorPoint.Y;
            }
            selectRect.Width = Math.Abs(mousePosition.X - anchorPoint.X);
            selectRect.Height = Math.Abs(mousePosition.Y - anchorPoint.Y);
            geometry.Rect = selectRect;
            AdornerLayer layer = AdornerLayer.GetAdornerLayer(adornedElement);
            layer.InvalidateArrange();
         }
      }

      private void EndSelection(object sender, MouseButtonEventArgs e)
      {
         const int MinSize = 3;

         if (selectRect.Width <= MinSize || selectRect.Height <= MinSize)
         {
            Rubberband.Visibility = Visibility.Hidden;
         }
         else
         {
            Window.CropButton.IsEnabled = true;
         }
         ReleaseMouseCapture();
      }

      protected override Visual GetVisualChild(int index)
      {
         return Rubberband;
      }
   }
}

Let’s unpack a bit of this code. You begin by creating a few private member variables:

private Rectangle Geometry geometry;
private UIElement adornedElement;
private Point anchorPoint;

The Geometry class is used to describe a 2-D shape. In this case, you’ll use the RectangleGeometry class to constrain the rubberband to draw a rectangle as the user drags the mouse across the photograph (as you saw in Figure 4-3).

The private member variable adornedElement is the element that will be adorned (i.e., the element on which the Rubberband will act). This value is passed into the constructor, which sends it along to the abstract base class (Adorner):

public RubberbandAdorner( UIElement adornedElement ) : base( adornedElement )

anchorPoint is the starting point for the rectangle, established by an initial mouse-down event similar to this:

private void OnMouseDown( object sender, MouseButtonEventArgs e )
{
   Point anchor = e.GetPosition( CurrentPhoto );
}

The constructor attaches the RubberbandAdorner to the adornedElement (in this case, the current photo) and creates a new rectangle:

this.adornedElement = adornedElement;
selectRect = new Rect();

It then sets the Rubberband property to a new Path object (a Path is used to describe a complex geometric figure; the segments within a path are combined to create a single shape):

Rubberband = new System.Windows.Shapes.Path();

In this case, you set the Data for the Path to geometry, which you’ll remember is just a RectangleGeometry (that is, the data to create a rectangle shape). The Path also takes a StrokeThickness (the width of the rectangle’s border), a Stroke (the color), an Opacity (the level of transparency), and a Visibility (it starts out hidden):

geometry = new RectangleGeometry();

Rubberband.Data = geometry;
Rubberband.StrokeThickness = 2;
Rubberband.Stroke = Brushes.Yellow;
Rubberband.Opacity = .6;
Rubberband.Visibility = Visibility.Hidden;

Next, you add the rubberband member variable to the adorner by calling its AddVisualChild() method:

AddVisualChild( Rubberband );

This adds the Path to the adorner’s collection of visual elements.

Finally, you add two event handlers, MouseMove (which calls DrawSelection()) and MouseUp (which calls EndSelection()):

MouseMove += new MouseEventHandler( DrawSelection );
MouseUp += new MouseButtonEventHandler( EndSelection );

StartSelection() is called by the OnMouseDown handler in Window1, as you’ll see later in this chapter.

Here is the complete listing of the constructor:

public RubberbandAdorner(UIElement adornedElement) : base(adornedElement)
{
   this.adornedElement = adornedElement;
   selectRect = new Rect();

   Rubberband = new System.Windows.Shapes.Path();

   geometry = new RectangleGeometry();

   Rubberband.Data = geometry;
   Rubberband.StrokeThickness = 2;
   Rubberband.Stroke = Brushes.Yellow;
   Rubberband.Opacity = .6;
   Rubberband.Visibility = Visibility.Hidden;
   AddVisualChild(Rubberband);
   MouseMove += new MouseEventHandler(DrawSelection);
   MouseUp += new MouseButtonEventHandler(EndSelection);
}

To place each visual child element, you call the base class’s ArrangeOverride() method and pass in the Size object you are given, getting back a Size object representing the area you have to work with. You then obtain each visual child in turn and call Arrange() on them, passing in a Rectangle. Finally, you return the Size object obtained from the base class:

protected override Size ArrangeOverride(Size size)
{
   Size finalSize = base.ArrangeOverride(size);
   ((UIElement)GetVisualChild(0)).Arrange(new Rect(new Point(), finalSize));
   return finalSize;
}

Business Classes

To support the user interface, you need a set of simple business classes representing the photographs and the items the user can purchase based on each photo. These are collected in the StoreItems.cs file, shown in Example 4-2.

Example 4-2. StoreItems.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Media.Imaging;
using System.Collections.Specialized;
using System.Windows.Controls;

namespace PhotoCooperative
{
   public class ImageFile
   {
      public String Path { get; set; }
      public Uri TheUri { get; set; }
      public BitmapFrame Image { get; set; }

      public ImageFile(string path)
      {
         Path = path;
         TheUri = new Uri(Path);
         Image = BitmapFrame.Create(TheUri);
      }

      public override string ToString()
      {
         return Path;
      }

   }

   public class PhotoList : ObservableCollection<Image File>
   {
      DirectoryInfo theDirectoryInfo;

      public PhotoList() { }

      public PhotoList(string path) : this(new DirectoryInfo(path)) { }

      public PhotoList(DirectoryInfo directory)
      {
         theDirectoryInfo = directory;
         Update();
      }

      public string Path
      {
         set
         {
            theDirectoryInfo = new DirectoryInfo(value);
            Update();
         }
         get { return theDirectoryInfo.FullName; }
      }

      public DirectoryInfo Directory
      {
         set
         {
            theDirectoryInfo = value;
            Update();
         }
         get { return theDirectoryInfo; }
      }

      private void Update()
      {
         foreach (FileInfo f in theDirectoryInfo.GetFiles("*.gif"))
         {
            Add(new ImageFile(f.FullName));
         }
      }
   }

   public class PrintType
   {
      public String Description { get; set; }
      public double Price { get; set; }

      public PrintType(string description, double price)
      {
         Description = description;
         Price = price;
      }

      public override string ToString()
      {
         return Description;
      }

   }

   public class PrintTypeList : ObservableCollection<PrintType>
   {
      public PrintTypeList()
      {
         Add(new PrintType("5x7 Print", 0.49));
         Add(new PrintType("Holiday Card", 1.99));
         Add(new PrintType("Sweatshirt", 19.99));
      }
   }

   public class PrintBase : INotifyPropertyChanged
   {
      private BitmapSource aPhoto;
      private PrintType aPrintType;
      private int aQuantity;

      public PrintBase(BitmapSource photo, PrintType printtype, int quantity)
      {
         Photo = photo;
         PrintType = printtype;
         Quantity = quantity;
      }

      public PrintBase(BitmapSource photo, string description, double cost)
      {
         Photo = photo;
         PrintType = new PrintType(description, cost);
         Quantity = 0;
      }

      public BitmapSource Photo
      {
         set { aPhoto = value; OnPropertyChanged("Photo"); }
         get { return aPhoto; }
      }

      public PrintType PrintType
      {
         set { aPrintType = value; OnPropertyChanged("PrintType"); }
         get { return aPrintType; }
      }

      public int Quantity
      {
         set { aQuantity = value; OnPropertyChanged("Quantity"); }
         get { return aQuantity; }
      }

      public event PropertyChangedEventHandler PropertyChanged;

      private void OnPropertyChanged(String info)
      {
         if (PropertyChanged != null)
             PropertyChanged(this, new PropertyChangedEventArgs(info));
      }

      public override string ToString()
      {
         return PrintType.ToString();
      }
   }

   public class Print : PrintBase
   {
      public Print(BitmapSource photo) : base(photo, "5x7 Print", 0.49) { }
   }

   public class GreetingCard : PrintBase
   {
      public GreetingCard(BitmapSource photo) : base(photo, "Greeting Card", 1.99) 
      { }
   }

   public class SShirt : PrintBase
   {
      public SShirt(BitmapSource photo) : base(photo, "Sweatshirt", 19.99) { }
   }

   public class PrintList : ObservableCollection<PrintBase> { }
}

There’s nothing terribly surprising in this code. In the ImageFile constructor, you first define the ImageFile object (the picture) based on its disk location. You create a URI from the path to the file on disk, and you create a BitmapFrame from that URI:

public ImageFile(string path)
{
   Path = path;
   TheUri = new Uri(Path);
   Image = BitmapFrame.Create(TheUri);
}

The PhotoList class represents an ObservableCollection of ImageFiles:

public class PhotoList : ObservableCollection<ImageFile>

ObservableCollection is a generic collection that implements the Observer pattern (discussed in Chapter 8); that is, it notifies interested (registered) objects when items are added or removed, or when the list is refreshed. This will come in handy later, when you create the scrolling listbox of photos from which the user can choose. Because the list is observable, when an item is selected an event will be raised. You can set an event handler accordingly:

<ListBox Style="{DynamicResource PhotoListStyle}"
    Grid.Row="1"
    Grid.ColumnSpan="3"
    Name ="PhotoListBox"
    Margin="0,0,0,20"
    DataContext="{Binding Source={StaticResource Photos}}"
    SelectionChanged ="PhotoListSelection"
    ItemsSource="{Binding }"
    ItemContainerStyle="{DynamicResource PhotoListItem}"
    SelectedIndex="0" />

You’ll need one more business class, to validate credit cards, but we’ll delay discussion of that until you’re ready to build the second page.

Page 1—Adding Items to the Shopping Cart

With the business classes in place in StoreItems.cs, you’re ready to build the first page. Open up Window1.xaml, as shown in Figure 4-4.

Now, edit this file to create a slider for the photographs. You’ll do this by adapting the image list created in the previous chapter to display the U.S. presidents.

In that example you had a Grid with a StackPanel, and inside that was a ListBox with ListBox items. In this case, rather than explicitly naming the items in the listbox, you’re going to bind the ListBox to a data source (a static resource called Photos). You’ll put the ListBox inside a Grid, and you’ll put that inside a ViewBox so the user can resize the entire thing. This will be done in two steps, with the first one allowing you to visualize the Grid.

The Photo Cooperative
Figure 4-4. The Photo Cooperative
<Viewbox VerticalAlignment="Top" Stretch="Uniform">

<Grid Margin="20" Width="620" ShowGridLines="True" >
   <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition Height="120" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="250" />
      <RowDefinition Height="15" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="*" />
   </Grid.RowDefinitions>
   <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition Width="400" />
      <ColumnDefinition Width="160" />
   </Grid.ColumnDefinitions>
   <TextBlock Grid.Row="0" Grid.ColumnSpan="3"
       Style="{DynamicResource TitleText}">
      <Span>The Photo Co-op: </Span>
      <Span FontStyle="Italic"> your pictures your way</Span>
   </TextBlock>
</Grid>

</Viewbox>

If you run the application now, you should see something like Figure 4-5. You won’t want the gridlines to be visible, though—to turn them off, set the ShowGridLines property of the Grid to False.

Running the application with the gridlines on
Figure 4-5. Running the application with the gridlines on

Next, you need to deal with some of the resources that the Window will use (specifically, DynamicResource and PhotoListStyle). It is helpful to understand that you simply need to add these resources to a Window.Resources section. You’ll put this section just below your Window class declaration:

<Window x:Class="PhotoCooperative.Window 1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Photo Cooperative" Height="600" Width="800"
    xmlns:sbts="clr-namespace:PhotoCooperative"
>
   <Window.Resources>

      <!-- PHOTOLIST TEMPLATE -->

      <Style x:Key="PhotoListStyle" TargetType="{x:Type ListBox}">
         <Setter Property="Template">
            <Setter.Value>
               <ControlTemplate TargetType="{x:Type ListBox}" >
                  <Border
                      BorderBrush="Gray"
                      BorderThickness="1"
                      CornerRadius="6"
                      Background="{DynamicResource ListBoxGradient}" >
                     <ScrollViewer
                         VerticalScrollBarVisibility="Disabled"
                         HorizontalScrollBarVisibility="Auto">
                        <StackPanel
                            IsItemsHost="True"
                            Orientation="Horizontal"
                            HorizontalAlignment="Left" />
                     </ScrollViewer>
                  </Border>
               </ControlTemplate>
            </Setter.Value>
         </Setter>
      </Style>

      <!-- PHOTOLIST STORYBOARDS -->

      <Style x:Key="PhotoListItem" TargetType="{x:Type ListBoxItem}">
         <Setter Property="MaxHeight" Value="75" />
         <Setter Property="MinHeight" Value="75" />
         <Setter Property="Opacity" Value=".75" />
         <Style.Triggers>
            <EventTrigger RoutedEvent="Mouse.MouseEnter">
               <EventTrigger.Actions>
                  <BeginStoryboard>
                     <Storyboard>
                        <DoubleAnimation
                            Duration="0:0:0.2"
                            Storyboard.TargetProperty="MaxHeight"
                            To="85" />
                        <DoubleAnimation
                            Duration="0:0:0.2"
                            Storyboard.TargetProperty="Opacity"
                            To="1.0" />
                     </Storyboard>
                  </BeginStoryboard>
               </EventTrigger.Actions>
            </EventTrigger>

            <EventTrigger RoutedEvent="Mouse.MouseLeave">
               <EventTrigger.Actions>
                  <BeginStoryboard>
                     <Storyboard>
                        <DoubleAnimation
                            Duration="0:0:1"
                            Storyboard.TargetProperty="MaxHeight" />
                        <DoubleAnimation
                            Duration="0:0:0.2"
                            Storyboard.TargetProperty="Opacity" />
                     </Storyboard>
                  </BeginStoryboard>
               </EventTrigger.Actions>
            </EventTrigger>
         </Style.Triggers>
      </Style>
   </Window.Resources>

At this point you will need to add the ListBox code mentioned earlier to your Window1.xaml file, right below the TextBlock inside the Grid:

<ListBox Style="{DynamicResource PhotoListStyle}"
    Grid.Row="1"
    Grid.ColumnSpan="3"
    Name ="PhotoListBox"
    Margin="0,0,0,20"
    DataContext="{Binding Source={StaticResource Photos}}"
    SelectionChanged ="PhotoListSelection"
    ItemsSource="{Binding }"
    ItemContainerStyle="{DynamicResource PhotoListItem}"
    SelectedIndex="0" />

Note the SelectionChanged attribute. This indicates that you need an event handler for when the selection changes in the photo display. Put that in Window1.xaml.cs:

private void PhotoListSelection( object sender, RoutedEventArgs e )
{
   String path = ( ( sender as ListBox ).SelectedItem.ToString() );
   BitmapSource img = BitmapFrame.Create( new Uri( path ) );
}

Recall that in StoreItems.cs you declared a class of type PhotoList:

public class PhotoList : ObservableCollection<ImageFile>

In Window1.xaml.cs, create a member variable that is an instance of this class:

public partial class Window1 : System.Windows.Window
{
   public PhotoList Photos;

The business class also defines an ImageFile class (see the listing for StoreItems.cs), but your Resources section needs to define a DataTemplate for binding to an ImageFile:

<!-- DATA TEMPLATES -->

<DataTemplate DataType="{x:Type sbts:ImageFile}">
   <Border VerticalAlignment="Center"
       HorizontalAlignment="Center"
       P adding="4"
       Margin="2"
       Background="White">
      <Image Source="{Binding Image}" />
   </Border>
</DataTemplate>

The rest is just aesthetics. You’ll want to define styles both for the Window itself and for the Title text:

<!-- STYLES -->

<Style TargetType="{x:Type sbts:Window1}">
   <Setter Property="Background"
       Value="{DynamicResource WindowGradient}" />
</Style>

<Style x:Key="TitleText"
    TargetType="{x:Type TextBlock}" >
   <Setter Property="FontFamily"
       Value="Segoe Black" />
   <Setter Property="FontSize"
       Value="20px" />
   <Setter Property="Foreground"
       Value="MidnightBlue" />
</Style>

Also, both the ListBox and the Window use gradients, so these must be defined as well. Add these brushes to the top of the Window.Resources section:

<!-- LINEAR GRADIENT BRUSHES -->
<LinearGradientBrush x:Key="WindowGradient"
    StartPoint="0,0.3"
    EndPoint="1,0">
   <GradientStop Color="#B2B6CAFF"
       Offset="0" />
   <GradientStop Color="#BFC3D5FF"
       Offset="0.1" />
   <GradientStop Color="#E0E4F0FF"
       Offset="0.3" />
   <GradientStop Color="#E6EAF5FF"
       Offset="0.5" />
   <GradientStop Color="#CFD7E2FF"
       Offset="0.6" />
   <GradientStop Color="#BFC5D3FF"
       Offset="0.8" />
   <GradientStop Color="#C4CBD8FF"
       Offset="1" />
</LinearGradientBrush>

<LinearGradientBrush x:Key="ListBoxGradient"
    StartPoint="0,0"
    EndPoint="0,1">

   <GradientStop Color="#90000000"
       Offset="0" />
   <GradientStop Color="#40000000"
       Offset="0.005" />
   <GradientStop Color="#10000000"
       Offset="0.04" />
   <GradientStop Color="#20000000"
       Offset="0.945" />
   <GradientStop Color="#60FFFFFF"
       Offset="1" />

</LinearGradientBrush>

You’re almost ready to run this puppy—you just have to tell the Application what your data source is. In App.xaml, add this code:

<Application x:Class="PhotoCooperative.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sbts="clr-namespace:PhotoCooperative"
    Startup="AppStartup">
   <Application.Resources>
      <ObjectDataProvider x:Name="PhotosODP" x:Key="Photos"
          ObjectType="{x:Type sbts:PhotoList}" />
   </Application.Resources>
</Application>

Then, in App.xaml.cs, add the following:

public partial class App : System.Windows.Application
{
   void AppStartup( object sender, StartupEventArgs args )
   {
      Window1 theWindow = new Window1();
      theWindow.Show();

      ObjectDataProvider dataProvider =
          this.Resources["Photos"] as ObjectDataProvider;

      PhotoList photoList = dataProvider.Data as PhotoList;
      theWindow.Photos = photoList;
      theWindow.Photos.Path = @"..\..\Photos";
   }
}

With all this wired together, you are ready to run your data-bound photo list (shown in Figure 4-6). The only thing left to do is create a Photos folder and put some GIF images in it. To add a new folder, right-click on the project and select Add → New Folder. Rename it Photos, then right-click on it and select Open in Windows Explorer. From there, you should be able to fill the folder with GIFs. If you need some GIFs in a hurry, you can download Alex’s from http://tinyurl.com/2jarve.

Image slider
Figure 4-6. Image slider

Displaying the Selected Image

You can add a display of the selected image with just a couple of small changes. To begin, just below the ListBox, add an Image control:

<Image Name="CurrentPhoto"
    Grid.Row="3"
    Grid.Column="1"
    Margin="10"
    MouseDown="OnMouseDown"/>

This depends on two things. The first is an OnMouseDown event handler, which you can stub out in Window1.xaml.cs:

private void OnMouseDown( object sender, MouseButtonEventArgs e )
{
}

The second is setting the CurrentPhoto when the user clicks in the image slider:

private void PhotoListSelection( object sender, RoutedEventArgs e )
{
   String path = ( ( sender as ListBox ).SelectedItem.ToString() );
   Bitmap Source img = BitmapFrame.Create( new Uri( path ) );
   CurrentPhoto.Source = img;
}

Presto! When the user selects an image in the slider, a nice blow-up of the image is shown below it, in the Image control you placed in grid row 3, column 1. Figure 4-7 demonstrates the effect.

The Image control
Figure 4-7. The Image control

Adding Cropping with the Adorner

There are two steps remaining to finish the first page. The first is to add the rubberband adorner discussed earlier, using the code detailed in Example 4-1. (We’ll deal with the second task—adding the shopping cart—in the next section.) Begin by adding a RubberbandAdorner class, which you’ll place in a new file called CropUtilities.cs.

To tie this into the application, you need to make a few changes in Window1.xaml. First, add the Crop and Undo buttons to the Grid:

<ListBox Style="{DynamicResource PhotoListStyle}"
    Grid.Row="1"
    Grid.ColumnSpan="3"
    Name ="PhotoListBox"
    Margin="0,0,0,20"
    DataContext="{Binding Source={StaticResource Photos}}"
    SelectionChanged ="PhotoListSelection"
    ItemsSource="{Binding }"
    ItemContainerStyle="{DynamicResource PhotoListItem}"
    SelectedIndex="0" />
<StackPanel
    Grid.Row="3"
    Grid.Column="0">

   <Button
       Name="CropButton"
       VerticalAlignment="Bottom"
       HorizontalAlignment="Center"
       Click="Crop"
       Width="55"
       Margin="2">
      Crop
   </Button>

</StackPanel>

<Button Grid.Row="3"
    Grid.Column="0"
    Name="UndoButton"
    VerticalAlignment="Bottom"
    HorizontalAlignment="Center"
    Click="Undo"
    IsEnabled="False"
    Width="55"
    Margin="2">
   Undo
</Button>

<Image Name="CurrentPhoto"
    Grid.Row="3"
    Grid.Column="1"
    Margin="10"
    MouseDown="OnMouseDown" />

These buttons need gradients, which you should now add to the Window.Resources section:

<LinearGradientBrush x:Key="ButtonGradient"
    StartPoint="0,0"
    EndPoint="0,1">

   <GradientStop Color="#FDB6CADF"
       Offset="0" />
   <GradientStop Color="#FCC3C5FF"
       Offset="0.1" />
   <GradientStop Color="#FCC4D0EF"
       Offset="0.3" />
   <GradientStop Color="#FDB7C2DF"
       Offset="0.6" />
   <GradientStop Color="#FE95B3CF"
       Offset="0.8" />
   <GradientStop Color="#FE96AACF"
       Offset="1" />

</LinearGradientBrush>

<LinearGradientBrush x:Key="ButtonUpGradient"
    StartPoint="0,0"
    EndPoint="0,1">

   <GradientStop Color="Transparent"
       Offset="0" />
   <GradientStop Color="#33000000"
       Offset="1" />

</LinearGradientBrush>

<LinearGradientBrush x:Key="ButtonDownGradient"
    StartPoint="0,0"
    EndPoint="0,1">

   <GradientStop Color="#10000000"
       Offset="0" />
   <GradientStop Color="#20000000"
       Offset="1" />

</LinearGradientBrush>

<LinearGradientBrush x:Key="ButtonDisabledGradient"
    StartPoint="0,0"
    EndPoint="0,1">

   <GradientStop Color="#10302A90"
       Offset="0" />
   <GradientStop Color="#10201040"
       Offset="1" />

</LinearGradientBrush>

You’ll also need to add some event handlers. One is obvious: you need to capture the OnMouseDown event to begin the rubberband. But you must also initialize the adorner. The best place to do this is in the Loaded event. To fire the Loaded event, add the following to the Window tag:

<Window x:Class="PhotoCooperative.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Photo Cooperative" Height="600" Width="800"
    xmlns:sbts="clr-namespace:PhotoCooperative"
    Loaded="WindowLoaded"
    >

This sets the event handler WindowLoaded() to handle the Loaded event. The event handlers, of course, go in Window1.xaml.cs. Make sure you reference the following namespaces:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Media.Animation;
using System.Windows.Input;
using System.Collections;

Here’s the top of that file:

public partial class Window 1 : System.Windows.Window
{
   public PhotoList Photos;
   private Stack UndoStack;
   private RubberbandAdorner CropSelector;

   public Window1()
   {
      InitializeComponent();
      UndoStack = new Stack();
   }

   private void WindowLoaded( object sender, EventArgs e )
   {
      AdornerLayer layer = AdornerLayer.GetAdornerLayer( CurrentPhoto );
      CropSelector = new RubberbandAdorner( CurrentPhoto );
      CropSelector.Window = this;
      layer.Add( CropSelector );
      CropSelector.Rubberband.Visibility = Visibility.Hidden;
   }

The constructor initializes the Undo stack, and then the WindowLoaded() event handler (which runs after the Window is, er, loaded) creates the rubberband. It does so by getting the AdornerLayer (the acetate layer that is placed “on top of” the element it adorns).

Tip

The static method GetAdornerLayer() walks up the “visual tree,” starting at CurrentPhoto, and returns the first adorner layer it finds.

CropSelector is a private member variable of type RubberbandAdorner (which itself is defined in CropUtilities.cs), and in WindowLoaded() you call its constructor, passing in the target element to be adorned (the current photo). You then set its Window property to the current window, add the RubberbandAdorner to the AdornerLayer, and set the RubberbandAdorner to invisible (awaiting the user’s mouse-down).

MouseDown

When the user clicks in the current photo, the MouseDown event fires. Here, you remember the point where the mouse was clicked. You then capture the mouse through the CropSelector and call its StartSelection() method. You also enable the CropButton:

private void OnMouseDown( object sender, MouseButtonEventArgs e )
{
   Point anchor = e.GetPosition( CurrentPhoto );
   CropSelector.CaptureMouse();
   CropSelector.StartSelection( anchor );
   CropButton.IsEnabled = true;
}

Because the mouse has been captured, the MouseUp event is handled by the RubberbandAdorner (as you saw in Example 4-1, excerpted here):

public RubberbandAdorner( UIElement adornedElement ) : base( adornedElement )
{
   this.adornedElement = adornedElement;
   //...
   MouseMove += new MouseEventHandler( DrawSelection );
   MouseUp += new MouseButtonEventHandler( EndSelection );
}

With this in place, you can expand the PhotoListSelection() method to manage the Undo stack and the CropSelector:

private void PhotoListSelection( object sender, RoutedEventArgs e )
{
   String path = ( ( sender as ListBox ).SelectedItem.ToString() );
   BitmapSource img = BitmapFrame.Create( new Uri( path ) );
   CurrentPhoto.Source = img;
   ClearUndoStack();
   if ( CropSelector != null )
   {
      if ( Visibility.Visible == CropSelector.Rubberband.Visibility )
          CropSelector.Rubberband.Visibility = Visibility.Hidden;
   }
   CropButton.IsEnabled = false;
}

Handling the Crop button

You now need to implement the method to call when the Crop button is clicked, which will crop the current picture to the limits of the rubberband adorner:

private void Crop( object sender, RoutedEventArgs e )
{
   if ( CurrentPhoto.Source != null )
   {
      BitmapSource img = ( BitmapSource ) ( CurrentPhoto.Source );
      UndoStack.Push( img );
      Int32Rect rect = new Int32Rect();
      rect.X = ( int ) ( CropSelector.SelectRect.X *
          img.PixelWidth / CurrentPhoto.ActualWidth );
      rect.Y = ( int ) ( CropSelector.SelectRect.Y *
          img.PixelHeight / CurrentPhoto.ActualHeight );
      rect.Width = ( int ) ( CropSelector.SelectRect.Width *
          img.PixelWidth / CurrentPhoto.ActualWidth );
      rect.Height = ( int ) ( CropSelector.SelectRect.Height *
          img.PixelHeight / CurrentPhoto.ActualHeight );
      CurrentPhoto.Source = new CroppedBitmap( img, rect );

      CropSelector.Rubberband.Visibility = Visibility.Hidden;

      CropButton.IsEnabled = false;
      UndoButton.IsEnabled = true;
   }
}

You start by obtaining the Source property of the CurrentPhoto. CurrentPhoto, you will remember, is defined to be of type Image; its Source property returns the image source. You cast it to a BitMapSource and store it in the local variable img, which you push onto the UndoStack (to be restored if the user clicks the Undo button).

Next, you create a rectangle and obtain its size by asking the CropSelector for its proportions. You set the CurrentPhoto’s image source to a new CroppedBitmap, which you created by passing in the original BitmapSource and the rectangle you just sized.

That done, you hide the rubberband, disable the Crop button, and enable the Undo button.

The picture is now cropped, but pressing the Undo button will undo the cropping:

private void Undo( object sender, RoutedEventArgs e )
{
   if ( UndoStack.Count > 0 )
   {
      CurrentPhoto.Source = ( BitmapSource ) UndoStack.Pop();
   }
   if ( UndoStack.Count == 0 )
   {
      UndoButton.IsEnabled = false;
   }
}

Adding the Shopping Cart

To finish the first page, you only need to add the shopping cart and the associated buttons. You want to offer the user the ability, having chosen a picture, to purchase a 5 x 7 photo, a sweatshirt, or a “holiday card.”

Begin by adding the shopping cart template and triggers to Window.Resources:

<!-- SHOPPING CART TEMPLATE -->

<Style x:Key="ShoppingCartStyle"
    TargetType="{x:Type ListBox}">
   <Setter Property="Template">
      <Setter.Value>
         <ControlTemplate TargetType="{x:Type ListBox}" >
            <Border BorderBrush="Gray"
                BorderThickness="1"
                CornerRadius="6"
                Background="{DynamicResource ShoppingCartGradient}" >
               <ScrollViewer>
                  <WrapPanel ItemHeight="70"
                      ItemWidth="70"
                      Margin="0,25,0,0"
                      IsItemsHost="True"
                      Orientation="Horizontal"
                      HorizontalAlignment="Center" />
               </ScrollViewer>
            </Border>
         </ControlTemplate>
      </Setter.Value>
   </Setter>
</Style>

<!-- SHOPPING CART TRIGGERS -->

<Style x:Key="ShoppingCartItem"
    TargetType="{x:Type ListBoxItem}">
   <Setter Property="BorderBrush"
       Value="Transparent" />
   <Setter Property="Template">
      <Setter.Value>
         <ControlTemplate TargetType="{x:Type ListBoxItem}">
            <Border x:Name="ContentBorder"
                Opacity="0.85">
               <ContentPresenter />
            </Border>
            <ControlTemplate.Triggers>
               <Trigger Property="IsSelected"
                   Value="True">
                  <Setter TargetName="ContentBorder"
                      Property="Opacity"
                      Value="1.0" />
               </Trigger>
            </ControlTemplate.Triggers>
         </ControlTemplate>
      </Setter.Value>
   </Setter>
</Style>

The ShoppingCart uses a ShoppingCartGradient, so of course you’ll need to add that to the Resources section with the other gradient brushes:

<LinearGradientBrush x:Key="ShoppingCartGradient" StartPoint="0,0" EndPoint="0,1">
   <GradientStop Color="#90000000" Offset="0" />
   <GradientStop Color="#40000000" Offset="0.002" />
   <GradientStop Color="#10000000" Offset="0.02" />
   <GradientStop Color="#20000000" Offset="0.98" />
   <GradientStop Color="#60FFFFFF" Offset="1" />
</LinearGradientBrush>

The trigger is set on the listbox, but the listbox it is set on is ShoppingCartItem, which we have not yet added. This is the drop-down from which the user selects an item to add to the cart. Add that combo box to your Window’s Resources section now:

<!-- COMBOBOX STYLE -->

<Style TargetType="{x:Type ComboBox}" >
   <Setter Property="Background"
       Value="{DynamicResource ComboBoxGradient}" />
   <Setter Property="BorderThickness"
       Value="0" />
   <Setter Property="Height"
       Value="18px" />
   <Setter Property="Foreground"
       Value="MidnightBlue" />
</Style>

<LinearGradientBrush x:Key="ComboBoxGradient" StartPoint="0,0" EndPoint="0,1">

   <GradientStop Color="#B2B6CAFF" Offset="0" />
   <GradientStop Color="#B0B3C5FF" Offset="0.1" />
   <GradientStop Color="#BEE4E0FF" Offset="0.3" />
   <GradientStop Color="#B0D7E2FF" Offset="0.6" />
   <GradientStop Color="#B0C5D3FF" Offset="0.8" />
   <GradientStop Color="#C4CBD8FF" Offset="1" />

</LinearGradientBrush>

With the styles in place, you only need to add the objects to the Grid. In this case, you’ll use an inner Grid:

<Image Name="CurrentPhoto"
    Grid.Row="3"
    Grid.Column="1"
    Margin="10"
    MouseDown="OnMouseDown"/>

   <Grid
       Grid.Row="5"
       Grid.Column="1"
       HorizontalAlignment="Center"
       Margin="0">
      <Grid.RowDefinitions>
         <RowDefinition Height="Auto"/>
         <RowDefinition Height="Auto"/>
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
         <ColumnDefinition/>
         <ColumnDefinition/>
      </Grid.ColumnDefinitions>
      <ComboBox
          Grid.Row="0"
          Grid.Column="0"
          Margin="0,0,4,0"
          VerticalAlignment="Center"
          Name="PrintTypeComboBox"
          DataContext="{Binding Source={StaticResource PrintTypes}}"
          ItemsSource="{Binding}"
          Width="110"
          SelectedIndex="0" />

      <Button
          Grid.Row="0"
          Grid.Column="1"
          Click="AddToShoppingCart"
          VerticalAlignment="Center"
          Width="100"
          IsDefault="True">
         Add To Cart
      </Button>

      <Button
          Grid.Row="1"
          Grid.Column="1"
          Name="RemoveButton"
          Click="RemoveShoppingCartItem"
          VerticalAlignment="Center"
          IsEnabled="False"
          Width="100"
          Margin="10" >
         Remove Item
      </Button>

   </Grid>

</Grid>

Because you’ve declared event handlers, you’ll have to stub them out in your code-behind:

private void AddToShoppingCart( object sender, RoutedEventArgs e )
{
}

private void RemoveShoppingCartItem( object sender, RoutedEventArgs e )
{
}

You’ll also need a few additional data templates for your visual shopping cart. Add them to the Resources section now:

<DataTemplate DataType="{x:Type sbts:Print}">
   <Grid Margin="3">
      <Image Source="baseImg/photoframe.gif" />
      <Image Source="{Binding Photo}"
          MaxWidth="50"
          MaxHeight="70"
          VerticalAlignment="Center"
          HorizontalAlignment="Center"/>
   </Grid>
</DataTemplate>

<DataTemplate DataType="{x:Type sbts:GreetingCard}">
   <Grid Margin="3" >
      <Border VerticalAlignment="Center"
          HorizontalAlignment="Center"
          Background="{DynamicResource GreetingCardGradient}"
          Width="40"
          Height="50"
          BorderBrush="#44000000"
          BorderThickness="1" >
         <Border.RenderTransform>
            <SkewTransform AngleY="-10" />
         </Border.RenderTransform>
      </Border>
      <Border VerticalAlignment="Center"
          HorizontalAlignment="Center"
          Background="White"
          Width="50"
          Height="50"
          BorderBrush="#66000000"
          BorderThickness="1" >
         <Image Margin="3"
             Source="{Binding Photo}" />
      </Border>
   </Grid>
</DataTemplate>

<DataTemplate DataType="{x:Type sbts:SShirt}">
   <Grid Margin="3">
      <Image Source="baseImg/sweatshirt-front.gif"/>
      <Image Source="{Binding Photo}"
          MaxWidth="20"
          MaxHeight="22.5"
          VerticalAlignment="Center"
          HorizontalAlignment="Center"/>
   </Grid>
</DataTemplate>

These new types need to be identified. Do that in the App.xaml file:

<Application x:Class="PhotoCooperative.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sbts="clr-namespace:PhotoCooperative"
    Startup="AppStartup"
    >

    <Application.Resources>
        <ObjectDataProvider x:Name="PhotosODP"
            x:Key="Photos"
            ObjectType="{x:Type sbts:PhotoList}" />
        <ObjectDataProvider x:Name="ShoppingCartODP"
            x:Key="ShoppingCart"
            ObjectType="{x:Type sbts:PrintList}" />
        <ObjectDataProvider x:Name="PrintTypesODP"
            x:Key="PrintTypes"
            ObjectType="{x:Type sbts:PrintTypeList}" />
    </Application.Resources>
</Application>

Then add the associated code to AppStartup() in App.xaml.cs:

dataProvider = this.Resources["ShoppingCart"] as ObjectDataProvider;
PrintList printList = dataProvider.Data as PrintList;
theWindow.ShoppingCart = printList;

And don’t forget to add the ShoppingCart private member variable to Window1.xaml.cs:

public partial class Window1 : System.Windows.Window
{
   public PhotoList Photos;
   public PrintList ShoppingCart;

With this additional code, your first page is 80% complete. Figure 4-8 shows the result.

Combo box and buttons added
Figure 4-8. Combo box and buttons added

Adding scroll bars

To finalize the page, you need to add the shopping cart to it. You’ll give the cart scroll bars so that users can scroll through the items they’ve added and, if desired, select items to remove from the cart.

As demonstrated earlier, you can begin by adding the resources you anticipate needing. Alternatively, you can add the widget and then add the resources it needs.

Let’s begin by adding the shopping cart inside a Grid and the associated upload button and progress bar within a StackPanel:

<Grid Grid.Row="3"
    Grid.Column="2">
   <Grid.RowDefinitions>
      <RowDefinition Height="20" />
      <RowDefinition />
   </Grid.RowDefinitions>
   <TextBlock Grid.Row="0"
       Foreground="MidnightBlue"
       FontSize="13px"
       Margin="2"
       HorizontalAlignment="Center">
      Shopping Cart
   </TextBlock>
   <ListBox ScrollViewer.HorizontalScrollBarVisibility="Disabled"
       Style="{DynamicResource ShoppingCartStyle}"
       Name="ShoppingCartListBox"
       Grid.Row="1"
       Width="160"
       DataContext="{Binding Source={StaticResource ShoppingCart}}"
       ItemContainerStyle="{DynamicResource ShoppingCartItem}"
       ItemsSource="{Binding}" />
</Grid>

<StackPanel
    Grid.Row="5"
    Grid.Column="2" >
   <Button
       Name="UploadButton"
       Click="Checkout"
       VerticalAlignment="Bottom"
       HorizontalAlignment="Center"
       Width="100"
       Margin="2"
       IsEnabled="False">
      Checkout
   </Button>

   <ProgressBar
       Name="UploadProgressBar"
       Grid.Row="6"
       Grid.Column="2"
       VerticalAlignment="Top"
       Margin="0,10,0,0" />
</StackPanel>

The shopping cart itself is the ListBox inside the Grid. You need a ShoppingCartStyle, a DataContext, and the ShoppingCartItem resource, all of which you’ve already created. What you need to create now are the scroll bar and progress bar resources:

<!-- PROGRESS BAR STYLE -->

<Style TargetType="{x:Type ProgressBar}" >
   <Setter Property="Background"
       Value="{DynamicResource ComboBoxGradient}" />
   <Setter Property="BorderThickness"
       Value="1" />
   <Setter Property="BorderBrush"
       Value="Gray" />
   <Setter Property="Foreground"
       Value="MidnightBlue" />
</Style>

<!--SCROLL BAR TEMPLATES -->

<Style x:Key="Scrollbar_LineButton"
    TargetType="{x:Type RepeatButton}">
   <Setter Property="Template">
      <Setter.Value>
         <ControlTemplate TargetType="{x:Type RepeatButton}">
            <Border BorderBrush="Transparent"
                BorderThickness="1"
                CornerRadius="6"
                Background="{DynamicResource ButtonGradient}">
               <ContentPresenter x:Name="ContentSite" />
            </Border>
         </ControlTemplate>
      </Setter.Value>
   </Setter>
   <Setter Property="MinHeight"
       Value="12" />
   <Setter Property="MinWidth"
       Value="12" />
   <Setter Property="Foreground"
       Value="Gray" />
   <Setter Property="FontSize"
       Value="6pt" />
   <Setter Property="FontWeight"
       Value="Bold" />
   <Setter Property="FontFamily"
       Value="Lucida Sans" />
   <Setter Property="VerticalAlignment"
       Value="Center" />
   <Setter Property="HorizontalAlignment"
       Value="Center" />
</Style>

<Style x:Key="ScrollBar_TrackRepeater"
    TargetType="{x:Type RepeatButton}">
   <Setter Property="IsTabStop"
       Value="false" />
   <Setter Property="Focusable"
       Value="false" />
   <Setter Property="Command"
       Value="ScrollBar.PageUpCommand" />
   <Setter Property="Template">
      <Setter.Value>
         <ControlTemplate TargetType="{x:Type RepeatButton}">
            <Rectangle Fill="Transparent" />
         </ControlTemplate>
      </Setter.Value>
   </Setter>
</Style>

<Style x:Key="ScrollBar_UpTrack"
    BasedOn="{StaticResource ScrollBar_TrackRepeater}"
    TargetType="{x:Type RepeatButton}">
   <Setter Property="Command"
       Value="ScrollBar.PageUpCommand" />
</Style>

<Style x:Key="ScrollBar_DownTrack"
    BasedOn="{StaticResource ScrollBar_TrackRepeater}"
    TargetType="{x:Type RepeatButton}">
   <Setter Property="Command"
       Value="ScrollBar.PageDownCommand" />
</Style>

<Style x:Key="ScrollBar_LeftTrack"
    BasedOn="{StaticResource ScrollBar_TrackRepeater}"
    TargetType="{x:Type RepeatButton}">
   <Setter Property="Command"
       Value="ScrollBar.PageLeftCommand" />
</Style>

<Style x:Key="ScrollBar_RightTrack"
    BasedOn="{StaticResource ScrollBar_TrackRepeater}"
    TargetType="{x:Type RepeatButton}">
   <Setter Property="Command"
       Value="ScrollBar.PageRightCommand" />
</Style>

<Style x:Key="ScrollBar_VerticalThumb"
    TargetType="{x:Type Thumb}">
   <Setter Property="Template">
      <Setter.Value>
         <ControlTemplate TargetType="{x:Type Thumb}">
            <Border CornerRadius="6"
                BorderBrush="Transparent"
                BorderThickness="1"
                Background="{DynamicResource VerticalScrollGradient}" />
         </ControlTemplate>
      </Setter.Value>
   </Setter>
   <Setter Property="MinHeight"
       Value="10" />
   <Setter Property="MinWidth"
       Value="10" />
</Style>

<Style x:Key="ScrollBar_HorizontalThumb"
    TargetType="{x:Type Thumb}">
   <Setter Property="Template">
      <Setter.Value>
         <ControlTemplate TargetType="{x:Type Thumb}">
            <Border CornerRadius="6"
                BorderBrush="Transparent"
                BorderThickness="1"
                Background="{DynamicResource ButtonGradient}" />
         </ControlTemplate>
      </Setter.Value>
   </Setter>
   <Setter Property="MinHeight"
       Value="10" />
   <Setter Property="MinWidth"
       Value="10" />
</Style>

<Style TargetType="{x:Type ScrollBar}">
   <Setter Property="Background"
       Value="Transparent" />
   <Setter Property="MinWidth"
       Value="10" />
   <Setter Property="Template">
      <Setter.Value>
         <ControlTemplate TargetType="{x:Type ScrollBar}">
            <Grid>
               <Grid.ColumnDefinitions>
                  <ColumnDefinition Width="10"/>
               </Grid.ColumnDefinitions>
               <Grid.RowDefinitions>
                  <RowDefinition Height="10"/>
                  <RowDefinition Height="*"/>
                  <RowDefinition Height="10"/>
               </Grid.RowDefinitions>
               <Border Grid.Row="1"
                   BorderThickness="0"
                   Background="Transparent"
                   CornerRadius="4"/>
               <RepeatButton Grid.Row="0"
                   Style="{DynamicResource Scrollbar_LineButton}"
                   Command="ScrollBar.LineUpCommand"
                   Content=" ^"/>
               <Track Grid.Row="1"
                   Name="PART_Track"
                   IsDirectionReversed="True">
                  <Track.IncreaseRepeatButton>
                     <RepeatButton Style="{DynamicResource ScrollBar_DownTrack}"/>
                  </Track.IncreaseRepeatButton>
                  <Track.DecreaseRepeatButton>
                     <RepeatButton Style="{DynamicResource ScrollBar_UpTrack}"/>
                  </Track.DecreaseRepeatButton>
                  <Track.Thumb>
                     <Thumb Style="{DynamicResource ScrollBar_VerticalThumb}"/>
                  </Track.Thumb>
               </Track>
               <RepeatButton Grid.Row="2"
                   Style="{DynamicResource Scrollbar_LineButton}"
                   Command="ScrollBar.LineDownCommand"
                   Content=" v"/>
            </Grid>
         </ControlTemplate>
      </Setter.Value>
   </Setter>
   <Style.Triggers>
      <Trigger Property="Orientation"
          Value="Horizontal" >
         <Setter Property="Background"
             Value="Transparent" />
         <Setter Property="MinHeight"
             Value="10" />
         <Setter Property="Template">
            <Setter.Value>
               <ControlTemplate TargetType="{x:Type ScrollBar}">
                  <Grid>
                     <Grid.RowDefinitions>
                        <RowDefinition Height="12"/>
                     </Grid.RowDefinitions>
                     <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="12" />
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="12" />
                     </Grid.ColumnDefinitions>
                     <Border Grid.Column="1"
                         BorderThickness="0"
                         Background="Transparent"
                         CornerRadius="4"/>
                     <RepeatButton Grid.Column="0"
                         Style="{DynamicResource Scrollbar_LineButton}"
                         Command="ScrollBar.LineLeftCommand"
                         Content=" &lt;"/>
                     <Track Grid.Column="1"
                         Name="PART_Track">
                        <Track.IncreaseRepeatButton>
                           <RepeatButton Style=
                               "{DynamicResource ScrollBar_RightTrack}"/>
                        </Track.IncreaseRepeatButton>
                        <Track.DecreaseRepeatButton>
                           <RepeatButton Style=
                               "{DynamicResource ScrollBar_LeftTrack}"/>
                        </Track.DecreaseRepeatButton>
                        <Track.Thumb>
                           <Thumb Style=
                               "{DynamicResource ScrollBar_HorizontalThumb}"/>
                        </Track.Thumb>
                     </Track>
                     <RepeatButton Grid.Column="2"
                         Style="{DynamicResource Scrollbar_LineButton}"
                         Command="ScrollBar.LineRightCommand"
                         Content=" &gt;"/>

                  </Grid>
               </ControlTemplate>
            </Setter.Value>
         </Setter>
      </Trigger>
   </Style.Triggers>
</Style>

Now it’s time to fill in the stubbed-out methods and add the CheckOut() method, shown in Example 4-3.

Example 4-3. Shopping cart methods for Window1.xaml.cs
private void AddToShoppingCart( object sender, RoutedEventArgs e )
{
   if ( PrintTypeComboBox.SelectedItem != null )
   {
      PrintBase item;
      switch ( PrintTypeComboBox.SelectedIndex )
      {
         case 0:
            item = new Print( CurrentPhoto.Source as BitmapSource );
            break;
         case 1:
            item = new GreetingCard( CurrentPhoto.Source as BitmapSource );
            break;
         case 2:
            item = new SShirt( CurrentPhoto.Source as BitmapSource );
            break;
         default:
            return;
      }
      ShoppingCart.Add( item );
      ShoppingCartListBox.ScrollIntoView( item );
      ShoppingCartListBox.SelectedItem = item;
      if ( false == UploadButton.IsEnabled )
         UploadButton.IsEnabled = true;
      if ( false == RemoveButton.IsEnabled )
         RemoveButton.IsEnabled = true;
   }
}

private void RemoveShoppingCartItem( object sender, RoutedEventArgs e )
{
   if ( null != ShoppingCartListBox.SelectedItem )
   {
      PrintBase item = ShoppingCartListBox.SelectedItem as PrintBase;
      ShoppingCart.Remove( item );
      ShoppingCartListBox.SelectedIndex = ShoppingCart.Count - 1;
   }
   if ( ShoppingCart.Count == 0 )
   {
      RemoveButton.IsEnabled = false;
      UploadButton.IsEnabled = false;
   }

private void Checkout( object sender, RoutedEventArgs e )
{
   if ( ShoppingCart.Count > 0 )
   {
      // go to checkout page
      // to be created later
   }
}

Finally, to finish off this page, you’ll need to add a baseImg folder to your project, then download the sweatshirt-front.gif and photoframe.gif images from our web sites so as to be able to populate the shopping cart (go to http://www.jliberty.com and click on “Books,” or go to http://alexhorovitz.com/books/programming3.5/).

If you look carefully, you’ll see that the sweatshirts in the shopping cart have the appropriate photo imposed on them, as shown in Figure 4-9. This is accomplished in the AddToShoppingCart() event handler, where, for example, a new SShirt object is instantiated, passing in the current photo as a BitMapSource to the constructor. The SShirt constructor is in StoreItems.cs; it passes the bitmap to its base class, where it is assigned to the private member variable of type BitMapSource and rendered appropriately.

Page 2—Validating the Credit Card

The second page, shown in Figure 4-10, is pretty straightforward to lay out.

Start by creating the new page, Checkout.xaml. (Be sure to add a new Page, not a new Windows Form—Pages are used for WPF, and Windows Forms are used for .NET 2.x.)

Once again, let’s start simple. You’ll add the TextBlock for the header and a Grid to hold the radio buttons for the credit cards, all of which will be in a Viewbox so the user can resize the display.

Page 1 finished
Figure 4-9. Page 1 finished
Page 2
Figure 4-10. Page 2
<Window x:Class="PhotoCooperative.Checkout"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Photo Cooperative: Checkout" Height="600" Width="800"
    xmlns:sbts="clr-namespace:PhotoCooperative"
    >
<Window.Resources>
   <LinearGradientBrush x:Key="WindowGradient"
       StartPoint="0,0.3" EndPoint="1,0">
      <GradientStop Color="#B2B6CAFF" Offset="0" />
      <GradientStop Color="#BFC3D5FF" Offset="0.1" />
      <GradientStop Color="#E0E4F0FF" Offset="0.3" />
      <GradientStop Color="#E6EAF5FF" Offset="0.5" />
      <GradientStop Color="#CFD7E2FF" Offset="0.6" />
      <GradientStop Color="#BFC5D3FF" Offset="0.8" />
      <GradientStop Color="#C4CBD8FF" Offset="1" />
   </LinearGradientBrush>

   <LinearGradientBrush x:Key="ButtonGradient"
       StartPoint="0,0" EndPoint="0,1">
      <GradientStop Color="#FDB6CADF" Offset="0" />
      <GradientStop Color="#FCC3C5FF" Offset="0.1" />
      <GradientStop Color="#FCC4D0EF" Offset="0.3" />
      <GradientStop Color="#FDB7C2DF" Offset="0.6" />
      <GradientStop Color="#FE95B3CF" Offset="0.8" />
      <GradientStop Color="#FE96AACF" Offset="1" />

   </LinearGradientBrush>

   <LinearGradientBrush x:Key="ButtonUpGradient"
       StartPoint="0,0" EndPoint="0,1">
      <GradientStop Color="Transparent" Offset="0" />
      <GradientStop Color="#33000000" Offset="1" />
   </LinearGradientBrush>

   <LinearGradientBrush x:Key="ButtonDownGradient"
       StartPoint="0,0" EndPoint="0,1">
      <GradientStop Color="#10000000" Offset="0" />
      <GradientStop Color="#20000000" Offset="1" />
   </LinearGradientBrush>

   <LinearGradientBrush x:Key="ButtonDisabledGradient"
       StartPoint="0,0" EndPoint="0,1">
      <GradientStop Color="#10302A90" Offset="0" />
      <GradientStop Color="#10201040" Offset="1" />
   </LinearGradientBrush>

<!-- STYLES -->
   <Style TargetType="{x:Type sbts:Checkout}">
      <Setter Property="Background"
          Value="{DynamicResource WindowGradient}" />
   </Style>

   <Style x:Key="TitleText" TargetType="{x:Type TextBlock}" >
      <Setter Property="FontFamily" Value="Segoe Black" />
      <Setter Property="FontSize" Value="20px" />
      <Setter Property="Foreground" Value="MidnightBlue" />
   </Style>

   <Style x:Key="CheckoutText" TargetType="{x:Type TextBlock}" >
      <Setter Property="FontFamily" Value="Segoe Black" />
      <Setter Property="FontSize" Value="14px" />
      <Setter Property="Foreground" Value="MidnightBlue" />
   </Style>

   <Style x:Key="InputText" TargetType="{x:Type TextBox}">
      <Setter Property="Height" Value="25px" />
      <Setter Property="FontFamily" Value="Segoe Black" />
      <Setter Property="Foreground" Value="#0066CC" />
      <Setter Property="FontSize" Value="10pt" />
      <Setter Property="Margin" Value="10,10,20,10" />
      <Style.Triggers>
         <Trigger Property="Validation.HasError" Value="true">
            <Setter Property="ToolTip"
                Value="{Binding RelativeSource={RelativeSource Self},
                Path=(Validation.Errors)[0].ErrorContent}"/>
         </Trigger>
         <Trigger Property="Validation.HasError" Value="false">
            <Setter Property="ToolTip"
                Value="{Binding RelativeSource={RelativeSource Self},
                Path=ToolTip.Content}"/>
         </Trigger>
      </Style.Triggers>
   </Style>
<!-- BUTTON TEMPLATE -->

   <Style TargetType="{x:Type Button}">
      <Setter Property="Template">
         <Setter.Value>
            <ControlTemplate TargetType="{x:Type Button}">
               <Border x:Name="OuterBorder"
                   CornerRadius="3"
                   Background="{DynamicResource ButtonGradient}">
                  <Border x:Name="InnerBorder"
                      CornerRadius="3"
                      Background="{DynamicResource ButtonUpGradient}"
                      Padding="{TemplateBinding Padding}">
                     <ContentPresenter x:Name="ContentSite"
                         HorizontalAlignment="Center"
                         VerticalAlignment="Center" />
                  </Border>
               </Border>
               <ControlTemplate.Triggers>
                  <Trigger Property="IsPressed" Value="true">
                     <Setter TargetName="InnerBorder"
                         Property="Background"
                         Value="{DynamicResource ButtonDownGradient}" />
                  </Trigger>
                  <Trigger Property="IsEnabled" Value="false">
                     <Setter TargetName="InnerBorder"
                         Property="Background"
                         Value="{DynamicResource ButtonDisabledGradient}" />
                     <Setter Property="BorderBrush" Value="Silver" />
                     <Setter Property="Foreground" Value="SlateGray" />
                  </Trigger>
               </ControlTemplate.Triggers>
            </ControlTemplate>
         </Setter.Value>
      </Setter>
      <Setter Property="Height" Value="18" />
      <Setter Property="Foreground" Value="MidnightBlue" />
   </Style>
</Window.Resources>
<Viewbox VerticalAlignment="Top" Stretch="Uniform">

   <Grid Margin="20" Width="650" ShowGridLines="False" >
      <Grid.RowDefinitions>
         <RowDefinition Height="Auto" />
         <RowDefinition Height="30" />
         <RowDefinition Height="50" />
         <RowDefinition Height="50" />
         <RowDefinition Height="50" />
         <RowDefinition Height="Auto" />
         <RowDefinition Height="*" />
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
         <ColumnDefinition Width="200" />
         <ColumnDefinition Width="50" />
         <ColumnDefinition Width="50" />
         <ColumnDefinition Width="50" />
         <ColumnDefinition Width="50" />
         <ColumnDefinition Width="50" />
      </Grid.ColumnDefinitions>

      <TextBlock Grid.Row="0" Grid.ColumnSpan="6"
          Style="{DynamicResource TitleText}">
         <Span>The Photo Co-op: Checkout</Span>
      </TextBlock>

      <TextBlock Grid.Row="2" Grid.Column="0"
          Style="{DynamicResource CheckoutText}"
          HorizontalAlignment="Right"
          VerticalAlignment="Center">
         <Span>Choose Payment Method:</Span>
      </TextBlock>

      <RadioButton Name="AmericanExpress" Grid.Row="2"
          Grid.Column="1" Click="OnCardSelected"
          VerticalAlignment="Center">

         <Image Source="baseImg/creditcardamex.gif"
             MaxWidth="38"
             MaxHeight="24"
             VerticalAlignment="Center"
             HorizontalAlignment="Center"/>
      </RadioButton>
      <RadioButton Name="Visa" Grid.Row="2"
          Grid.Column="2" Click="OnCardSelected"
          VerticalAlignment="Center">
         <Image Source="baseImg/creditcardvisa.gif"
             MaxWidth="38"
             MaxHeight="24"
             VerticalAlignment="Center"
             HorizontalAlignment="Center"/>
      </RadioButton>
      <RadioButton Name="MasterCard" Grid.Row="2"
          Grid.Column="3" Click="OnCardSelected"
          VerticalAlignment="Center">
         <Image Source="baseImg/creditcardmastercard.gif"
             MaxWidth="38"
             MaxHeight="24"
             VerticalAlignment="Center"
             HorizontalAlignment="Center"/>
      </RadioButton>
      <RadioButton Name="Discover" Grid.Row="2"
          Grid.Column="4" Click="OnCardSelected"
          VerticalAlignment="Center">
         <Image Source="baseImg/creditcarddiscover.gif"
             MaxWidth="38"
             MaxHeight="24"
             VerticalAlignment="Center"
             HorizontalAlignment="Center"/>
      </RadioButton>

      <TextBlock Grid.Row="3" Grid.Column="0"
          Style="{DynamicResource CheckoutText}"
          TextAlignment="Right" VerticalAlignment="Center">
         <Span>Name on Card:</Span>
      </TextBlock>

      <TextBox Style="{StaticResource InputText}"
          Grid.Column="1"
          Grid.Row="3"
          Grid.ColumnSpan="4"
          Name="nameOnCard"
          Width="150"
          VerticalAlignment="Center">
         <TextBox.ToolTip>Enter your name.</TextBox.ToolTip>
      </TextBox>

      <TextBlock Grid.Row="4" Grid.Column="0"
          Style="{DynamicResource CheckoutText}"
          TextAlignment="Right"
          VerticalAlignment="Center">
         <Span>Card Number:</Span>
      </TextBlock>
      <TextBox Style="{StaticResource InputText}"
          Grid.Column="1"
          Grid.Row="4"
          Grid.ColumnSpan="4"
          Name="ccNumber" Width="150"
          VerticalAlignment="Center">
         <TextBox.ToolTip>
            Enter valid credit card number.
         </TextBox.ToolTip>
      </TextBox>
      <Button Name="ProcessOrder" Grid.ColumnSpan="3"
          Grid.Column="1"
          Grid.Row="5"
          Click="ProcessOrderForCart">
         Process my credit card!
      </Button>
      <Label Name="ProcessResults"
          Grid.Column="1"
          Grid.Row ="6"
          Grid.ColumnSpan="4"
          TextBlock.Foreground="Red" />
   </Grid>
</Viewbox>
</Window>

Tip

This code assumes that you downloaded the four .gif files for the four credit cards (creditcardamex.gif, creditcardmastercard.gif, creditcardvisa.gif and creditcarddiscover.gif) into the baseImg directory while you were downloading the .gif files required for the previous section.

You are probably already scanning this code to see what code-behind support you need, and what resources you’ll want to add, and no doubt you’ve discovered these hints:

Click="OnCardSelected"
Style="{DynamicResource CheckoutText}"
Style="{DynamicResource TitleText}">

First add the two styles to the Resources section:

<Window.Resources>
   <!-- STYLES -->

   <Style x:Key="TitleText"
       TargetType="{x:Type TextBlock}" >
      <Setter Property="FontFamily"
          Value="Segoe Black" />
      <Setter Property="FontSize"
          Value="20px" />
      <Setter Property="Foreground"
          Value="MidnightBlue" />
   </Style>

   <Style x:Key="CheckoutText"
       TargetType="{x:Type TextBlock}" >
      <Setter Property="FontFamily"
          Value="Segoe Black" />
      <Setter Property="FontSize"
          Value="14px" />
      <Setter Property="Foreground"
          Value="MidnightBlue" />
   </Style>

</Window.Resources>

Then, in the code-behind, you can stub out the required event handler:

public void OnCardSelected( object sender, EventArgs e )
{

}

To see your shopping cart page, return to the first page and fill in the details in the Checkout event handler that you stubbed out earlier:

private void Checkout( object sender, RoutedEventArgs e )
{
   if ( ShoppingCart.Count > 0 )
   {
      Checkout co = new Checkout();
      co.ShoppingCart = ShoppingCart;
      co.Show();
      this.Hide();
   }
}

This code now makes an instance of your new (second) page and sets its ShoppingCart property to the ShoppingCart object created on the first page. It then shows the second page and hides the first.

Modify your ShoppingCart class to have a ShoppingCart property to match what the first page will set:

public partial class Checkout : System.Windows.Window
{
   private PrintList shoppingCart;
   public PrintList ShoppingCart { set { shoppingCart = value; } }

Tip

The class PrintList, you will remember, is defined in your StoreItems.cs file, as an ObservableCollection of PrintBase. Print, GreetingCard, and SShirt (SweatShirt) all derive from PrintBase.

Layout

The rest of the layout is pretty straightforward. You need a place for the user’s name and credit card number, and a button to submit the order:

<TextBlock Grid.Row="3" 
    Grid.Column="0"
    Style="{DynamicResource CheckoutText}"
    TextAlignment="Right"
    VerticalAlignment="Center">
   <Span>Name on Card:</Span>
</TextBlock>

<TextBox Style="{StaticResource InputText}"
    Grid.Column="1"
    Grid.Row="3"
    Grid.ColumnSpan="4"
    Name="nameOnCard"
    Width="150"
    VerticalAlignment="Center">
   <TextBox.ToolTip>Enter your name.</TextBox.ToolTip>
</TextBox>

<TextBlock Grid.Row="4"
    Grid.Column="0"
    Style="{DynamicResource CheckoutText}"
    TextAlignment="Right"
    VerticalAlignment="Center">
   <Span>Card Number:</Span>
</TextBlock>
<TextBox Style="{StaticResource InputText}"
    Grid.Column="1"
    Grid.Row="4"
    Grid.ColumnSpan="4"
    Name="ccNumber"
    Width="150"
    VerticalAlignment="Center">
   <TextBox.ToolTip>Enter valid credit card number.</TextBox.ToolTip>
</TextBox>
<Button Name="ProcessOrder"
    Grid.ColumnSpan="3"
    Grid.Column="1"
    Grid.Row="5"
    Click="ProcessOrderForCart">Process my credit card!</Button>
<Label Name="ProcessResults"
    Grid.Column="1"
    Grid.Row ="6"
    Grid.ColumnSpan="4"
    TextBlock.Foreground="Red" />

Notice the use of tooltips attached to the TextBlocks. When the user hovers over the text entry block, a tooltip will provide additional information, as shown in Figure 4-11.

Tooltip
Figure 4-11. Tooltip

Validating the Credit Card

When the user clicks the “Process my credit card!” button, you’d like to validate the credit card number before submitting the information to the credit card company. You’ll do so using the Luhn algorithm (also known as the modulus 10 algorithm), created by IBM scientist Hans Peter Luhn and described in U.S. Patent 2,950,048, filed on January 6, 1954 and granted on August 23, 1960 (according to Wikipedia).

To accomplish this, take the following steps:

  1. Create a new business class, CreditCardValidator, as a C# class, and create a new enumerated constant, CardBrand.

  2. When the user clicks on a credit card, you’ll set the chosen CardBrand.

  3. When the user clicks the “Process my credit card!” button, you’ll call the static method Validate() in your new class.

Here’s the listing for CreditCardValidator.cs:

using System;
using System.Collections.Generic;
using System.Text;

namespace PhotoCooperative
{
   public enum CardBrand
   {
      NotSelected,
      MasterCard,
      BankCard,
      Visa,
      AmericanExpress,
      Discover,
      DinersClub,
      JCB
   };

   public static class CreditCardValidator
   {
      public static bool Validate(CardBrand cardBrand,
          string cardNumber)
      {

         byte[] number = new byte[16]; // card number to validate

            // Remove non-digits
            int length = 0;
            for (int i = 0; i < cardNumber.Length; i++)
            {
               if (char.IsDigit(cardNumber, i))
               {
                  if (length == 16) return false; // card has too
                                                  // many digits
                  number[length++] = byte.Parse(cardNumber[i].ToString());
               }
            }

            // To validate a card, you need to
            // test length then prefix...
            switch (cardBrand)
            {
               case CardBrand.BankCard:
                  if (length != 16)
                     return false;
                  if (number[0] != 5 || number[1] != 6
                      || number[2] > 1)
                     return false;
                  break;

               case CardBrand.MasterCard:
                  if (length != 16)
                     return false;
                  if (number[0] != 5 || number[1] == 0
                      || number[1] > 5)
                     return false;
                  break;

               case CardBrand.Visa:
                  if (length != 16 && length != 13)
                     return false;
                  if (number[0] != 4)
                     return false;
                  break;

               case CardBrand.AmericanExpress:
                  if (length != 15)
                     return false;
                  if (number[0] != 3 || (number[1] != 4
                      && number[1] != 7))
                     return false;
                  break;

               case CardBrand.Discover:
                  if (length != 16)
                     return false;
                  if (number[0] != 6 || number[1] != 0
                      || number[2] != 1 || number[3] != 1)
                     return false;
                  break;

               case CardBrand.DinersClub:
                  if (length != 14)
                     return false;
                  if (number[0] != 3 || (number[1] != 0
                      && number[1] != 6 && number[1] != 8)
                      || number[1] == 0 && number[2] > 5)
                     return false;
                  break;
            }

         // Now we use the classic Luhn algorithm to validate
         int sum = 0;
         for (int i = length - 1; i >= 0; i--)
         {
            if (i % 2 == length % 2)
            {
               int n = number[i] * 2;
               sum += (n / 10) + (n % 10);
            }
            else
               sum += number[i];
         }
         return (sum % 10 == 0);

      }
   }
}

When the user clicks on a credit card, the OnCardSelected() event handler in Checkout.xaml.cs is called:

public void OnCardSelected( object sender, EventArgs e )
{
   RadioButton rb = sender as RadioButton;
   string rbName = rb.Name;
   switch ( rbName )
   {
      case "Visa":
         selectedCard = CardBrand.Visa;
         break;
      case "MasterCard":
         selectedCard = CardBrand.MasterCard;
         break;

      case "AmericanExpress":
         selectedCard = CardBrand.AmericanExpress;
         break;
      default:
         selectedCard = CardBrand.Discover;
         break;
   }
}

Finally, when the user clicks the “Process my credit card!” button, the ProcessOrderForCart() method in Checkout.xaml.cs is called:

public void ProcessOrderForCart( object sender, RoutedEventArgs e )
{
   String creditCardNumber = ccNumber.Text;

   if ( selectedCard == CardBrand.NotSelected )
   {
      MessageBox.Show( "Please select a credit card type", "Uh oh",
          MessageBoxButton.OK, MessageBoxImage.Error );
   }
   else
   {
      if ( CreditCardValidator.Validate( selectedCard, creditCardNumber ) )
      {
         ProcessResults.Content = "Validated";
      }
      else
      {
         ProcessResults.Content = "Excuse me sir, there's a call for you...";
      }
   }
}

That’s it! A meaningful WPF application, with all the fixins.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required