WPF Bssics
WPF Bssics
Foundation Using C#
Student Guide
Revision 4.0
Student Guide
Information in this document is subject to change without notice. Companies, names and data used
in examples herein are fictitious unless otherwise noted. No part of this document may be
reproduced or transmitted in any form or by any means, electronic or mechanical, for any purpose,
without the express written permission of Object Innovations.
Product and company names mentioned herein are the trademarks or registered trademarks of their
respective owners.
Object Innovations
877-558-7246
www.objectinnovations.com
Chapter 1
Introduction to WPF
Introduction to WPF
Objectives
Why WPF?
XAML
Controls
Data Binding
Appearance
Graphics
Media
Plan of Course
namespace FirstWpf
{
public class MainWindow : Window
{
[STAThread]
static void Main(string[] args)
{
Application app = new Application();
app.Run(new MainWindow());
}
public MainWindow()
{
Title = "Welcome to WPF (Code)";
Width = 288;
Height = 192;
}
}
}
Creating a Button
Content = btn;
}
7. Build the project. Youll get a compile error, because you need
an additional namespace, System.Windows.Controls.
using System;
using System.Windows;
using System.Windows.Controls;
8. Build and run. Youll se the button fills the whole client area of
the main window.
9. Add the following code to specify the horizontal and vertical
alignment of the button.
btn.HorizontalAlignment =
HorizontalAlignment.Center;
btn.VerticalAlignment = VerticalAlignment.Center;
10. Build and run. Now the button will be properly displayed,
sized just large enough to contain the buttons text in the
designated font.
Content = btn;
}
12. Build and run. You will now see a message box displayed
when you click the Say Hello button
13. You can specify the initial input focus by calling the Focus()
method of the Button class (inherited from the UIElement
class).
btn.Focus();
14. Build and run. The button will now have the initial input
focus, and hitting the Enter key will invoke the buttons Click
event handler. You are now at Step 2.
See FirstWpf\Step2.
using System;
using System.Windows;
using System.Windows.Controls;
namespace FirstWpf
{
public class MainWindow : Window
{
[STAThread]
static void Main(string[] args)
{
Application app = new Application();
app.Run(new MainWindow());
}
public MainWindow()
{
Title = "Welcome to WPF (Code)";
Width = 288;
Height = 192;
btn.Click += ButtonOnClick;
// Setting focus is deprecated for
// violating accessibility guidelines
btn.Focus();
Content = btn;
}
Device-Independent Pixels
Class Hierarchy
Content Property
Content = btn;
Simple Brushes
Panels
Children of Panels
panel.Children.Add(btnGreet);
Example TwoControls
TwoControls Code
public TwoControls()
{
Title = "Two Controls Demo";
Width = 288;
const int MARGINSIZE = 10;
Automatic Sizing
SizeToContent = SizeToContent.Height;
panel.Background = Brushes.Beige;
panel.Margin = new Thickness(MARGINSIZE);
Note that we are specifying a brush for the panel, and we are
specifying a margin of 10 device-independent pixels.
Lab 1
Summary
Lab 1
Introduction
In this lab you will implement the TwoControls example program from scratch. This
example will illustrate in detail the steps needed to create a new WPF application using
Visual Studio 2010, and you will get practice with all the fundamental concepts of WPF
that weve covered in this chapter.
In Part 1 you will use Visual Studio to create a WPF application. You will go on to create
a StackPanel that has as children a TextBox and a Button. This first version does not
provide an event handler for the button. Also, it does not handle sizing very well!
1. Use Visual Studio to create a new WPF application TwoControls in the Lab1 folder.
4. In Program.cs enter the following code, which does the minimum of creating
Application and Window objects.
using System;
using System.Windows;
using System.Windows.Controls;
namespace TwoControls
{
class TwoControls : Window
{
[STAThread]
static void Main(string[] args)
{
Application app = new Application();
app.Run(new TwoControls());
}
public TwoControls()
{
}
}
}
5. Build and run. You should get a clean compile. You should see a main window,
which has no title and an empty client area.
7. Build and run. Now you should see a title and the width as specified.
8. Now we are going to set the Content of the main window to a new StackPanel that we
create. To be able to visually see the StackPanel, we will paint the background with a
beige brush, and well make the Margin of the StackPanel 10 device-independent
pixels.
public TwoControls()
{
Title = "Two Controls Demo";
Width = 288;
const int MARGINSIZE = 10;
panel.Background = Brushes.Beige;
panel.Margin = new Thickness(MARGINSIZE);
}
9. Build. Youll get a compiler error because you need a new namespace for the
Brushes class.
10. Bring in the System.Windows.Media namespace. Now you should get a clean build.
Run your application. You should see the StackPanel displayed as solid beige, with a
small margin.
11. Next we will add a TextBox as a child of the panel. Since we will be referencing the
TextBox in an event-handler method as well as the constructor, define a private data
member txtName of type TextBox.
private TextBox txtName;
12. Provide the following code to initialize txtName and add it as a child to the panel.
txtName = new TextBox();
txtName.FontSize = 16;
txtName.HorizontalAlignment = HorizontalAlignment.Center;
txtName.Width = Width / 2;
panel.Children.Add(txtName);
13. Build and run. Now you should see the TextBox displayed, centered, at the top of the
panel.
14. Next, add code to initialize a Button and add it as a child to the panel.
Button btnGreet = new Button();
btnGreet.Content = "Say Hello";
btnGreet.FontSize = 16;
btnGreet.HorizontalAlignment = HorizontalAlignment.Center;
panel.Children.Add(btnGreet);
15. Build and run. You should now see the two controls in the panel. You are now at
Step1.
In Part 2 you will handle the Click event of the button. You will also provide better
layout of the two controls.
1. First, well handle the Click event for the button. Provide this code to add a handler
for the Click event.
btnGreet.Click += ButtonOnClick;
2. Provide this code for the handler, displaying a greeting to the person whose name is
entered in the text box.
void ButtonOnClick(object sender, RoutedEventArgs args)
{
MessageBox.Show("Hello, " + txtName.Text, "Greeting");
}
3. Build and run. The program now has its functionality, but the layout needs improving.
4. Provide the following code to size the height of the window to the size of its content.
SizeToContent = SizeToContent.Height;
5. Build and run. Now the vertical sizing of the window is better, but the controls are
jammed up against each other.
Chapter 2
XAML
XAML
Objectives
What Is XAML?
Default Namespace
<Button HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="16"
Click="Button_Click">
Say Hello
</Button>
</Application.Resources>
</Application>
</Grid>
</Window>
</Grid>
</Window>
3. Build and run. You should see the new title displayed, and the
window will be the dimension you specified.
10. Provide the following code for the event handler, which will
display a message box.
private void Button_Click(object sender,
RoutedEventArgs e)
{
MessageBox.Show("Hello, WPF", "Greeting");
}
Layout in WPF
Controlling Size
Margin
Control
Padding
Height
Content
Width
Thickness Structure
Children of Panels
panel.Children.Add(btnSayHello);
Example TwoControlsXaml
TwoControls XAML
Automatic Sizing
TwoControls Code
Orientation
Access Keys
Content Property
Lab 2
Type Converters
Summary
Lab 2
Introduction
In this lab you will incrementally create a XAML version of a Calculator program.
Youll first create the user interface. Then youll implement the programs functionality.
Finally, youll add some enhancements to the program.
1. Use Visual Studio to create a new WPF application Calculator in the Lab2 folder.
2. Edit the starter XAML to make the title Calculator, and replace the grid by a stack
panel with beige background.
<Window x:Class="Calculator.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Calculator" Height="300" Width="300">
<StackPanel Background="Beige">
</StackPanel>
</Window>
3. Provide XAML for the top pair of label and text box, for the first operand. They
should be inside a nested stack panel with horizontal orientation.
<StackPanel Background="Beige">
<StackPanel Orientation="Horizontal">
<Label Margin="10">
Operand _1:
</Label>
<TextBox Margin="10"
Width="72"
Name="txtOp1"/>
</StackPanel>
</StackPanel>
4. Provide the XAML for the second pair of label and text box. Build and run. You
should see your controls, but also a lot of extra space in the window.
6. Build and run. You should see the window with the controls youve laid out so far,
reasonably sized.
7. Add another horizontal stack panel with a pair of buttons for Add and Subtract.
<StackPanel Orientation="Horizontal">
<Button Margin="10"
Width="60"
Name="btnAdd">
Add
</Button>
<Button Margin="10"
Width="60"
Name="btnSubtract">
Subtract
</Button>
</StackPanel>
8. Add a fourth horizontal stack panel with another label and text box pair, for the
answer. This time the text box should be read-only.
9. Build and run. Your basic user interface is now complete, and you are at Step 1.
1. Add a handler for the Click event of the Add button. The simplest way to do this is to
double-click on the Add button in Design view.
2. In the event handler provide code to convert the strings entered for the operands to
numbers, add these numbers, and store the answer.
private void btnAdd_Click(object sender, RoutedEventArgs e)
{
int num1 = Convert.ToInt32(txtOp1.Text);
int num2 = Convert.ToInt32(txtOp2.Text);
int answer = num1 + num2;
txtAns.Text = answer.ToString();
}
4. Build and run. Do the Alt + 1 and Alt + 2 access keys work to position focus at the
first and second text boxes, respectively? You are at Step 2.
Part 3. Enhancements
1. Although we see the 1 and 2 characters underlined, the Alt access keys do not work
yet. Provide a Target attribute on the first label.
<Label Margin="10"
Target="{Binding ElementName=txtOp1}">
Operand _1:
</Label>
<TextBox Margin="10"
Width="72"
Name="txtOp1"/>
3. Add access key support for the buttons simply by providing an underscore in front of
the desired letter.
<Button Margin="10"
Width="60"
Name="btnAdd" Click="btnAdd_Click">
_Add
</Button>
<Button Margin="10"
Width="60"
Name="btnSubtract" Click="btnSubtract_Click">
_Subtract
</Button>
5. Add XAML for a check box indicating whether or not to center the numeric values.
<CheckBox Margin="10" Name="chkCenter">
Center Numeric Values
</CheckBox>
6. Build and run. The caption for the check box is jammed up against the box.
7. Achieve a better appearance by using the Content attribute notation and provide a
leading space.
<CheckBox Margin="10" Name="chkCenter"
Content=" Center Numeric Values">
</CheckBox>
9. Add handlers for the Checked and Unchecked events of the check box. When adding
the handler for Unchecked, do not add a new handler, but rather choose the already
existing handler for Checked. Note that by double-clicking the check box in Design
view you will add the same handler for both events.
10. Implement this common handler. Note that you will need to cast IsChecked to bool.
private void chkCenter_Checked(object sender, RoutedEventArgs e)
{
if ((bool)chkCenter.IsChecked)
{
txtOp1.TextAlignment = TextAlignment.Center;
txtOp2.TextAlignment = TextAlignment.Center;
txtAns.TextAlignment = TextAlignment.Center;
}
else
{
txtOp1.TextAlignment = TextAlignment.Left;
txtOp2.TextAlignment = TextAlignment.Left;
txtAns.TextAlignment = TextAlignment.Left;
}
}
11. Build and run. Your little calculator should now be fully functional! Youre at Step 3.
Chapter 7
Objectives
Toolbars in WPF
There are two toolbars, one for just Add and the other for the
remaining arithmetic operations.
The screenshot shows the second toolbar dragged below the
first.
Images on Buttons
Tool Tips
Status Bars
Lab 7
Summary
Lab 7
Introduction
In this lab you will enhance your simple editor by providing a toolbar and a status bar.
You will provide an icon on a number of menu items, corresponding to the toolbar button
images. You will also implement a checkable menu item to show or hide the status bar.
1. If you would like to continue with your own version of the Editor, delete the supplied
starter files and replace them with your files from the Lab6B folder. Build and run
your starter code.
2. Add the following to the XAML file, after the definition of the menu. Note that the
various image files are in the directory C:\OIC\Data\Grahpics.
<ToolBarTray Name="tbTray"
DockPanel.Dock="Top">
<ToolBar>
<Button Command="New">
<Image Source="c:\OIC\Data\Graphics\new.png" />
</Button>
<Button Command="Open">
<Image Source="c:\OIC\Data\Graphics\open.png" />
</Button>
<Button Command="Save">
<Image Source="c:\OIC\Data\Graphics\save.png" />
</Button>
<Separator/>
<Button Command="Cut">
<Image Source="c:\OIC\Data\Graphics\cut.png" />
</Button>
<Button Command="Copy">
<Image Source="c:\OIC\Data\Graphics\copy.png" />
</Button>
<Button Command="Paste">
<Image Source="c:\OIC\Data\Graphics\paste.png" />
</Button>
</ToolBar>
</ToolBarTray>
3. Build and run. Youll hit an exception. If you run under the debugger, youll see that
the exception is thrown on the first statement of EditorCanExecute(). At this point,
txtData has not yet been initialized. Why did a similar problem not occur with
menus?
4. The toolbar is always visible, but the menus have to be pulled down by the user. Thus
EditorCanExecute() wont be called before there is some interaction from the user.
5. Now lets fix the problem. Include in EditorCanExecute() a test for txtData being
null.
private void EditorCanExecute(object sender, CanExecuteRoutedEventArgs
e)
{
if (txtData == null)
e.CanExecute = false;
else if (txtData.Text != "")
e.CanExecute = true;
else
e.CanExecute = false;
}
6. Build and run. You should now have a functional toolbar without having written any
additional procedural code!
7. Try to see any difference in appearance of a disabled toolbar button from one that is
not disabled.
8. The disabled buttons are not grayed out, but a subtle difference is that the enabled
buttons are highlighted while the mouse hovers over them.
9. Add tool tips. For example, this XAML will add a tool tip for the New button.
<Button Command="New"
ToolTip="New">
<Image Source="c:\OIC\Data\Graphics\new.png" />
</Button>
10. Build and run. Now you can see an additional behavior difference with disabled
buttons: the tool tip is not shown.
11. Enhance the menu items by providing an icon with image from the corresponding
toolbar button. For example, this XAML will provide an icon for the New menu item.
<MenuItem Header="_New"
Command="New">
<MenuItem.Icon>
<Image Source="c:\OIC\Data\Graphics\new.png"/>
</MenuItem.Icon>
</MenuItem>
12. Add a combo box to the toolbar for specifying the font family. Also add a text box for
specifying the font size. There should be a separator between the previous buttons and
these new items on the toolbar.
<Separator/>
<ComboBox Name="cmbFontFamily"
Width="150"
SelectionChanged="cmbFont_SelectionChanged">
</ComboBox>
<TextBox Name="txtFontSize"
Width="50"
SelectionChanged="cmbFont_SelectionChanged">
</TextBox>
13. Provide code to initialize the combo box with the system font families and the text
box with the current font size. You can use the same code that you had in connection
with the font dialog box.
public Editor()
{
InitializeComponent();
foreach (FontFamily fam in Fonts.SystemFontFamilies)
cmbFontFamily.Items.Add(fam.Source);
txtFontSize.Text = txtData.FontSize.ToString();
cmbFontFamily.Text = txtData.FontFamily.Source;
}
14. Implement the common handler for the SelectionChanged event of the combobox.
void cmbFont_SelectionChanged(object sender, RoutedEventArgs args)
{
txtData.FontSize = Convert.ToDouble(txtFontSize.Text);
txtData.FontFamily =
new FontFamily(cmbFontFamily.SelectedItem.ToString());
}
15. Build and run. You get a crash, because the handler is called before txtData has been
initialized.
16. Provide a bool data member init, set to false initially and to true after
InitializeComponent() has been called.
private bool init = false;
public Editor()
{
InitializeComponent();
foreach (FontFamily fam in Fonts.SystemFontFamilies)
cmbFontFamily.Items.Add(fam.Source);
txtFontSize.Text = txtData.FontSize.ToString();
cmbFontFamily.Text = txtData.FontFamily.Source;
init = true;
}
18. Build and run. You should now be able to change the font using the controls on the
toolbar. Exercise all the functionality of your little editor.
2. Add three label controls to the status bar with content Char, Line and Col.
There should be a separator between them.
<Label Name="lblChar" Width="60">
Char
</Label>
<Separator/>
<Label Name="lblLine" Width="60">
Line
</Label>
<Separator/>
<Label Name="lblCol" Width="60">
Col
</Label>
<TextBox Name="txtData"
TextWrapping="Wrap"
AcceptsReturn="True"
VerticalScrollBarVisibility="Auto"
SelectionChanged="txtData_SelectionChanged">
</TextBox>
4. Implement the handler in the code-behind file. Char represents a zero-based index
of the current character position of the insertion point, which can be found from the
SelectionStart data member. Line represents the line number, starting from 1. This
can be found via the GetLineIndexFromCharacterIndex() method. Col
represents the column position starting from 1 in the current line. This can be found
from the GetCharacterIndexFromLineIndex() method.
private void txtData_SelectionChanged(object sender, RoutedEventArgs
args)
{
int iChar = txtData.SelectionStart;
lblChar.Content = string.Format("Char {0}", iChar);
int iLine = txtData.GetLineIndexFromCharacterIndex(iChar);
lblLine.Content = string.Format("Line {0}", iLine + 1);
int iCol = iChar - txtData.GetCharacterIndexFromLineIndex(iLine);
lblCol.Content = string.Format("Col {0}", iCol + 1);
}
5. Build and run. Observe how the information in the status bar changes as you type or
move about the insertion point.
6. As a final touch, add a new StatusBar menu item to the View menu. This is
checkable and indicates whether the status bar is visible or not. It is initialized to be
checked. There is a common handler for the Checked and Unchecked events.
<MenuItem Header="_StatusBar"
IsCheckable="True"
IsChecked="True"
Checked="OnStatusChecked"
Unchecked="OnStatusChecked">
</MenuItem>
7. Implement OnStatusChecked() in the code behind file. Be sure to test that init is
true.
private void OnStatusChecked(object sender, RoutedEventArgs args)
{
if (init)
{
MenuItem item = sender as MenuItem;
if (item.IsChecked)
statEditor.Visibility = Visibility.Visible;
else
statEditor.Visibility = Visibility.Collapsed;
}
}
8. Build and run. Exercise the various features of your little editor, which should now be
fully functional.
Chapter 10
Data Binding
Data Binding
Objectives
In this example:
Were using a ComboBox as source and a Label as target.
The path is SelectedItem.Content, which means that we want
the binding object to use the text of the selected item in the
ComboBox as the relevant source information.
Then the binding is set to a label, so that the selected item in
the ComboBox will always be shown in the Label.
Binding in XAML
Binding to a Collection
InitializeComponent();
}
Lab 10A
Binding to a Collection
In this lab you will enhance our first binding example by using a
collection to store the U.S. states shown in the ComboBox. You
will declare the collection in the procedural code and use it in
XAML, and will modify the Add button handler to update the
collection instead of the ComboBox.Items collection.
Data Context
3. Build and run the application. Notice that if you change the
selected state, the lblStatus label will receive the update since
SelectedItem is a dependency property. However, if you add
more states, the ComboBox isnt updated because the source
collection doesnt implement change notification. Lets fix this
by changing the collection type.
...
public static ObservableCollection<String> states;
public ComboBox()
{
states = new ObservableCollection<string>();
...
4. Build and run to see the result. Try adding a state to the
ComboBox to see the updated list and the count in the
lblStateCount Label.
5. Take a look again at the XAML code in the StatusBar, modified
in step 2. The reference to the cmbStates element appears
twice, and this situation suggests the use of data context. Add a
DataContext property to the StatusBar referencing cmbStates
and remove the ElementName argument from the bindings.
<StatusBar DockPanel.Dock="Bottom"
DataContext="{Binding ElementName=cmbStates}">
Selected state:
<Label Name="lblStatus"
Content="{Binding Path=SelectedItem}">
</Label>
| State count:
<Label Name="lblStateCount"
Content="{Binding Path=Items.Count}">
</Label>
6. Build and run the application. Test it to see that the binding is
working. Notice that the data context cannot be used outside the
StatusBar scope.
The application at this point is saved in the
DataContext\Step2 folder in the chapter directory.
7. Lets modify our solution to have the same implementation, but
using procedural code instead of XAML. To do this, remove the
DataContext property from the StatusBar definition in the
ComboBox.xaml file.
8. Now, add a name to the StatusBar, so that well be able to use it
in the procedural code. The XAML code for the StatusBar will
look like this:
<StatusBar DockPanel.Dock="Bottom"
Name="statusBar">
InitializeComponent();
statusBar.DataContext = cmbStates;
}
10. Build and run the solution to see the result. There is a copy of
this solution in the DataContext\Step3 folder in the chapter
directory.
Data Templates
Value Converters
Collection Views
Sorting
Grouping
view.GroupDescriptions.Clear();
view.GroupDescriptions.Add(
new PropertyGroupDescription("Region"));
Grouping Example
Filtering
Filtering Example
Data Providers
ObjectDataProvider
ObjectDataProvider Example
XmlDataProvider
XmlDataProvider Example
Lab 10B
SmallPub Database
1
If it fails, you may be able to work around the problem by modifying the connection. Click the Advanced
button and try setting the User Instance to False.
SQL Server
MySql
3. Click the link Add New Data Source, or use the menu Data |
Add New Data Source.
4. Select Database for the source where the application will get its
data. Click Next.
10. Observe that the model file Model1.edmx has been created,
and you are shown a graphical representation of the Book entity.
11. Examine the Data Sources window. Choose Details from the
dropdown next to Books.
14. Examine the XAML. You will see that data bindings have
been created for you.
15. Examine the C# code. Query code has been created to access
the data model.
private ObjectQuery<Book>
GetBooksQuery(SmallPubEntities context)
{
// Auto generated code
Navigation Code
private void btnFirst_Click(object sender,
RoutedEventArgs e)
{
bookView.MoveCurrentToFirst();
}
DataGrid Control
6. Build and run. You will see the book data displayed in the
DataGrid! See screen capture on the following page. Wed like
to remove the columns EntitySet and EntityKey. Also, the main
window is too big.
7. Edit the XAML for the main window to remove the explicit
Height and Width and instead use the SizeToContent property.
Also specify a suitable title.
<Window x:Class="SimpleDataGrid.MainWindow"
...
Title="Simple DataGrid"
SizeToContent="WidthAndHeight"
...
Loaded="Window_Loaded">
8. Build and run. The DataGrid should now fit neatly in the main
window without extra space except for the designated margin.
9. Now lets tackle the problem of the two columns we dont want.
Add a handler for the DataGrids AutoGeneratingColumn event.
Class Library
static DB()
{
context = new SmallPubEntities();
foreach (Book b in context.Books)
{
Debug.WriteLine(
"{0} {1, -20} {2} {3}",
b.BookId, b.Title, b.CategoryId,
b.PubYear);
}
}
Database Updates
Summary
Lab 10A
Binding to a Collection
Introduction
In this lab you will enhance our first binding example by using a collection to store the
U.S. states shown in the ComboBox. You will declare the collection in the procedural
code and use it in XAML, and will modify the Add button handler to update the
collection instead of the ComboBox.Items collection.
1. Build and run your starter code. Notice that you can add new states by typing in the
name of the state in the ComboBox and clicking the Add button. There is a label with
the selected state in the bottom, which is updated when the selected index of the
ComboBox is modified. There is another label with the state count, which is updated
when new states are added to the list.
2. Lets create our collection that will be used as a source by the cmbStates ComboBox.
Declare a new states collection of type Collection<String> in the
MainWindow.xaml.cs file. Youll need to import the namespace
System.Collections.ObjectModel.
...
using System.Collections.ObjectModel;
namespace ComboBoxBinding
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private Collection<String> states;
public ComboBox()
{
...
3. Now, lets populate the newly created collection with the values that are currently
being defined in the ComboBox XAML code. Besides adding these values to the
collection, we need to add it to the Window Resources collection to allow it to be
visible by the XAML elements.
public MainWindow()
{
states = new Collection<string>();
states.Add("California");
states.Add("Massachusetts");
states.Add("Illinois");
this.Resources.Add("states", states);
InitializeComponent();
}
4. In the MainWindow.xaml file, remove the list of ComboBox item elements defined
in XAML for the cmbStates ComboBox. Include the ItemsSource property defining
a Binding to the states collection, which can be referenced as a resource here.
<ComboBox
Name="cmbStates"
FontSize="16"
HorizontalAlignment="Center"
Margin="10"
Width="180"
IsEditable="True"
ItemsSource="{Binding Source={StaticResource states}}"
>
</ComboBox>
5. Build and run the application. Notice that the ComboBox has its Items collection
attached to the states collection defined in the procedural code. The state count label
appears to be showing the correct information, but the label for the selected state
stopped working. Additionally, if you try to add a new state using the Add button,
youll hit a runtime exception. Lets first fix the add item feature by modifying the
Click handler of the button to add the new item to the states collection, instead of the
ComboBox.Items collection.
private void BtnAdd_Click(object sender, RoutedEventArgs e)
{
states.Add(cmbStates.Text);
cmbStates.SelectedIndex = cmbStates.Items.Count - 1;
}
6. Build and run. Try adding a new item to the ComboBox. Despite not hitting any
exception, there is no visible update to the ComboBox.Items or the state count label.
Actually, the states collection was updated in the procedural code, but this update is
not seen by the controls in XAML because the Collection<T> class doesnt
implement the INotifyPropertyChanged interface. This can be easily fixed by
changing the collection type to ObservableCollection<T>, which implements the
INotifyPropertyChanged interface.
public partial class MainWindow: Window
{
private ObservableCollection<String> states;
public ComboBox()
{
states = new ObservableCollection<string>();
...
7. Build and run the application, and try adding a new state again. Youll notice the new
state added to the ComboBox.Items list and the new state count in the bottom label.
However, the label for the selected state still doesnt get updated. If you take a look at
the Path property of the binding in this label, youll see that it is bound to the
SelectedItem.Content property of the ComboBox, which was right for the previous
Items definition of the control using the ComboBoxItem elements. Considering that
the ComboBox is now bound to a collection of strings, each item of the ComboBox is
actually a simple string object, and the Path property of the label binding can be
simply SelectedItem.
<Label Name="lblSelectedState"
Content="{Binding ElementName=cmbStates, Path=SelectedItem}">
</Label>
8. Build and run the application. Now the selected item label is working properly, and
you can test the other window features to see that the program is fully functional.
Lab 10B
Introduction
In this lab you will implement an account manager which relies on data from an external
XML file. The starter code provides the interface, and you will provide code for XML
binding, adding/removing accounts and saving data back to the XML file.
1. Build and run the starter code. There is a ComboBox for account selection, three text
boxes for showing account information, and three action buttons for account
management. As the first thing we need here is data, lets add an XML file to the
project. Right-click the project name in solution explorer and select the Add
submenu, then click on the New Item option.
2. From the Add New Item dialog, select the XML File template and provide
AccountsData.xml as the file name. Click Add.
4. The purpose of the newly created XML file is to be an external source of data for our
program. Thus, it would be nice to have this XML file in the same directory of the
application executable, as a content resource (not embedded). To achieve this, modify
the file properties by right-clicking it and selecting Properties from the menu. Then,
change Build Action to Content and Copy to output Directory to Copy Always.
5. Build the application. Then, go to the bin\Debug directory and note that
AccountsData.xml is there. Now our file is ready to be used!
6. Define a XmlDataProvider resource in XAML pointing to the XML file. Give this
resource a name using the x:Key property so that it can be referenced. Additionally,
provide Accounts as the value for the XPath property, as this is the parent node of
the accounts list in our XML file.
<Window x:Class="AccountManager.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="ManageAccounts" SizeToContent="WidthAndHeight"
ResizeMode="CanMinimize">
<Window.Resources>
<XmlDataProvider x:Key="dataProvider"
XPath="Accounts"
Source="AccountsData.xml"/>
</Window.Resources>
...
7. Now, lets use the data provider in the ComboBox to obtain the accounts list. Using
the ComboBoxs ItemsSource property, set a binding to the dataProvider resource.
<ComboBox
Name="cmbAccounts"
Margin="10"
Width="96"
ItemsSource="{Binding Source={StaticResource dataProvider},
XPath=Account}"
>
</ComboBox>
8. Build and run the application. If you click on the ComboBox, youll notice an
unwanted behavior: the information contained in the list seems concatenated, since
the ComboBox is trying to show all the text from each item, which are XML nodes.
This has happened because we didnt tell the ComboBox which information from
each XML node should be used to display it. Lets fix this behavior by adding the
DisplayMemberPath property.
<ComboBox
Name="cmbAccounts"
Margin="10"
Width="96"
ItemsSource="{Binding Source={StaticResource dataProvider},
XPath=Account}"
DisplayMemberPath="Name"
>
</ComboBox>
9. Now, if you build and run the application, the list should be displayed properly by the
ComboBox. You have successfully bound the ComboBox to an XML data source!
10. A small tweak is to display the first element in the ComboBox, at index 0. Add a
Loaded event handler for the main window and set the SelectedIndex to 0.
private void Window_Loaded(object sender, RoutedEventArgs e)
{
cmbAccounts.SelectedIndex = 0;
}
11. Build and run. The program at this point is saved in the AccountManager\Step1
folder in the chapter directory.
Part 2. Bind the Text Boxes to the XML File Based on the Selected Item
1. Now we must add logic to populate the text boxes with account information based on
the currently selected account from the ComboBox. Well accomplish this by simply
reading the XML node from the ComboBox.SelectedItem property. Lets start by
defining a function to be responsible for binding the text boxes and the source XML
node. This node will be passed as a parameter to this function, and youll need to
import the System.Xml namespace into the class file. Note that the binding code for
each TextBox does pretty much the same thing as the XAML binding code for the
ComboBox.
private void BindAccountInformation(XmlNode account)
{
Binding numberBinding = new Binding();
numberBinding.Source = account;
numberBinding.XPath = "Number";
txtNumber.SetBinding(TextBox.TextProperty, numberBinding);
3. In procedural code, add implementation to the handler. It must get the Xml node from
the SelectedItem property of the ComboBox and call the recently created
BindAccountInformation() method passing it as a parameter. It will be a nice
protection if you clear the text boxes and its bindings in case there is no item selected
in the ComboBox.
private void cmbAccounts_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
int index = cmbAccounts.SelectedIndex;
if (index != -1)
{
XmlElement elem = (XmlElement)cmbAccounts.Items[index];
BindAccountInformation(elem);
}
else
{
BindingOperations.ClearAllBindings(txtNumber);
txtNumber.Text = "";
BindingOperations.ClearAllBindings(txtName);
txtName.Text = "";
BindingOperations.ClearAllBindings(txtBalance);
txtBalance.Text = "";
}
}
4. Build and run the application. Change the selected account using the ComboBox to
see the data changing in the text boxes, which is the expected behavior. However,
lets assume that it is important to protect the Number property, avoiding changes to
its value. To do so, set the IsReadOnly property of the txtNumber TextBox to True
and the Background to LightGray, to give it the visual aspect of a read only field.
<TextBox
Name="txtNumber"
Margin="10"
Width="72"
IsReadOnly="True"
Background="LightGray"
>
</TextBox>
1. In Part 2, you provided binding for the text boxes and an XML node that we got from
the ComboBox, which has the entire XML document as data source. The interesting
thing is that the WPF binding mechanism takes care of updating this XML data
source when we change the data in the text boxes. However, these changes are not
saved to the external XML file because the data source keeps only an in-memory
copy of the document. To save this copy into the external XML file, we can add code
to the Save button in our program by using the XmlDataProvider.Document.Save()
method. Double-click the Save button to add an event handler and provide code for
saving the file.
private void btnSave_Click(object sender, RoutedEventArgs e)
{
try
{
XmlDataProvider dp =
this.FindResource("dataProvider") as XmlDataProvider;
dp.Document.Save("AccountsData.xml");
MessageBox.Show("The data was successfully saved" , "Accounts");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Accounts");
}
}
2. Build and run the application. Try modifying some values in the text boxes for some
accounts, and click the Save button. Close the application and go to the bin\Debug
folder in the project directory, and open the AccountsData.xml file and note that
your changes were saved! If you open the application again, youll see that your
modified data was persisted. (Dont rebuild the application, or the starting data will
be copied over the modified data.)
3. Now lets add code for the Add button. Before creating a new account, well need to
provide a valid account number, which can be an increment of the biggest account
number that currently exists in the list. Provide a method that searches the accounts
list for the biggest account number and return this number plus 1 as a suggestion for
the new account number. The method name should be GetNextAccountNumber().
private int GetNextAccountNumber()
{
int maxAccountNumberFound = 0;
4. To create a new account, what you need to do is basically create a new XML node
with a blank account and add it to the XML document contained in the data provider
used by the ComboBox. By doing this, the ComboBox will be updated with this new
item automatically, and your last step will be to select this new items index so that
the user can edit the data using the text boxes.
private void btnAdd_Click(object sender, RoutedEventArgs e)
{
try
{
XmlDataProvider dp =
this.FindResource("dataProvider") as XmlDataProvider;
XmlNode accountNode =
dp.Document.CreateNode(XmlNodeType.Element, "Account", "");
XmlNode numberNode =
dp.Document.CreateNode(XmlNodeType.Element, "Number", "");
numberNode.InnerText = GetNextAccountNumber().ToString();
accountNode.AppendChild(numberNode);
XmlNode nameNode =
dp.Document.CreateNode(XmlNodeType.Element, "Name", "");
nameNode.InnerText = "New name";
accountNode.AppendChild(nameNode);
XmlNode balanceNode =
dp.Document.CreateNode(XmlNodeType.Element, "Balance", "");
balanceNode.InnerText = "0";
accountNode.AppendChild(balanceNode);
dp.Document.DocumentElement.AppendChild(accountNode);
cmbAccounts.SelectedIndex = cmbAccounts.Items.Count - 1;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Accounts");
}
}
5. Finally, add code for the Delete button. One solution you could use to delete an item
is to get the XML node contained in the SelectedItem property and then call the
RemoveChild() method in the XML Document, which will result in an immediate
update in the ComboBox.
private void btnDelete_Click(object sender, RoutedEventArgs e)
{
try
{
int index = cmbAccounts.SelectedIndex;
if (index != -1)
{
XmlDataProvider dp =
this.FindResource("dataProvider") as XmlDataProvider;
XmlElement elem = (XmlElement)cmbAccounts.Items[index];
dp.Document.DocumentElement.RemoveChild(elem);
cmbAccounts.SelectedIndex = 0;
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Accounts");
}
}
6. Build and run the application. Now your program should be fully functional, and you
can test the Add, Delete and Save buttons thoroughly.