Introduction
This article shows two implementations of the same simple program. First, we examine the Windows Forms version, followed by the WPF version. The purpose of this article is to show WinForms programmers a simple example of creating a WPF application. Along the way, we compare and contrast working with the two platforms.
This article does not shower WPF or WinForms with praise, but simply discusses the similarities and differences between how to develop with them. I did not write this article with the intention of persuading people to either move to WPF, or continue using Windows Forms. It simply reviews the experiences I had creating the same program on each platform, and shows how to create a simple program in WPF.
I wrote both applications in C# using Visual Studio 2008.
Background
Many WinForms developers create user interfaces by drag-and-dropping controls on the WinForms design surface in Visual Studio. I created the demo WinForms application that way, too. In order to create a meaningful comparison between developing on the two platforms, I also used the WPF design surface in Visual Studio 2008 (a.k.a. Cider) to develop the WPF version. I did not clean up the XAML generated by Cider. I did have to edit the XAML a little bit, as we’ll examine later.
I must admit, as of Visual Studio 2008, I am not exactly fond of using Cider. I usually write my XAML by hand, instead of having Cider spew it out for me. Beyond the fact that my XAML is cleaner and better formatted than Cider’s XAML, I find it easier to understand the structure of a UI when I write the XAML by hand. You might be gawking at the prospect of writing all that XML by hand, but I can assure you that it is a very productive mode of operation, once you reach a certain level of proficiency.
Microsoft recognizes that most people think I am crazy for preferring to write XAML by hand, and is working hard to improve the WPF design-time story. Cider currently leaves much to be desired for those of us accustomed to the great design-time support offered by a seasoned technology like WinForms. We will examine some of the differences later.
This article does not go too deep into WPF, nor does it explain the platform features being used in any depth. If you are new to WPF and would like to learn more about it, you can check out my five-part article series, ‘A Guided Tour of WPF’. Also, be sure to read Sacha Barber’s introductory articles about WPF, the first of which is here.
Overview of the Program
The demo application that this article focuses on is very simple. It does not do much, besides showing some employees of an imaginary company. The user can edit each employee’s name, but there is no input validation checking for empty values. I tried to make the WinForms and WPF versions look very similar. I also tried to do both platforms justice by making the best use of each that I know how.
Here is a screenshot of the WinForms version:
Here is a screenshot of the WPF version:
If you edit an employee’s first or last name and move the input focus to another field, that employee’s full name will update to reflect the change. This shows that the controls, in each version of the application, are bound to an Employee
object’s properties.
Shared Business Objects
Both applications use the same Employee
class from the same BusinessObjects class library project. That class is identical for both the WinForms application and the WPF application. There are no preprocessor blocks including/excluding any code based on the UI platform, or anything like that. Here is the class definition (keep in mind, this is C# 3.0 code):
public class Employee : INotifyPropertyChanged
{
#region Creation
public static Employee[] GetEmployees()
{
return new Employee[]
{
new Employee(1, "Joe", "Smith",
GetPictureFile(1), new DateTime(2000, 2, 12)),
new Employee(2, "Frank", "Green",
GetPictureFile(2), new DateTime(2002, 7, 1)),
new Employee(3, "Martha", "Piccardo",
GetPictureFile(3), new DateTime(2003, 1, 20)),
};
}
private static string GetPictureFile(int employeeID)
{
string fileName = String.Format("emp{0}.jpg", employeeID);
string folder = Path.GetDirectoryName(
Assembly.GetExecutingAssembly().Location);
folder = Path.Combine(folder, "Images");
return Path.Combine(folder, fileName);
}
private Employee(int id, string firstName, string lastName,
string pictureFile, DateTime startDate)
{
this.ID = id;
this.FirstName = firstName;
this.LastName = lastName;
this.PictureFile = pictureFile;
this.StartDate = startDate;
}
#endregion
#region Properties
public int ID { get; private set; }
string _firstName;
public string FirstName
{
get { return _firstName; }
set
{
if (value == _firstName)
return;
_firstName = value;
this.OnPropertyChanged("FirstName");
this.OnPropertyChanged("FullName");
}
}
string _lastName;
public string LastName
{
get { return _lastName; }
set
{
if (value == _lastName)
return;
_lastName = value;
this.OnPropertyChanged("LastName");
this.OnPropertyChanged("FullName");
}
}
public string FullName
{
get { return String.Format("{0}, {1}",
this.LastName, this.FirstName); }
}
public string PictureFile { get; private set; }
public DateTime StartDate { get; private set; }
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
There’s nothing unusual here, just some run-of-the-mill C# code. Notice that Employee
implements the INotifyPropertyChanged
interface, which both WinForms and WPF understand. That will come into play later, when binding against the FullName
property.
The WinForms Version
The Windows Forms program has a Form
and a custom UserControl
, called EmployeeControl
. The Form
contains a FlowLayoutPanel
, which contains one instance of EmployeeControl
for each Employee
. Here is the Form
’s code that I wrote:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
foreach(Employee emp in Employee.GetEmployees())
{
EmployeeControl empCtrl = new EmployeeControl();
empCtrl.Employee = emp;
this.flowLayoutPanel.Controls.Add(empCtrl);
}
}
}
EmployeeControl
, which is a UserControl
, was arranged on the Visual Studio design surface. Since each EmployeeControl
displays property values of one Employee
object, I used the excellent WinForms design-time support to get the “schema” of an Employee
object and then establish the data bindings in the Properties window. When I told Visual Studio that my EmployeeControl
will be bound to an instance of the Employee
class from the BusinessObjects assembly, it automatically created a BindingSource
component configured with metadata about Employee
for me. This made it a breeze to bind up (almost) all of the controls.
In case you are not familiar with this very useful tool, it is shown in the following screenshot, displaying how the lastNameTextBox
has its Text
property bound to the LastName
property of employeeBindingSource
:
I was able to create all of my data bindings by using that tool, except for the binding between the PictureBox
’s tooltip and the Employee
’s ID
property. I had to set that up in code. The code I wrote for the EmployeeControl
is shown below:
public partial class EmployeeControl : UserControl
{
public EmployeeControl()
{
InitializeComponent();
Binding binding = this.employeePicture.DataBindings[0];
binding.Format += this.ConvertFilePathToBitmap;
}
void ConvertFilePathToBitmap(object sender, ConvertEventArgs e)
{
e.Value = Bitmap.FromFile(e.Value as string);
}
public Employee Employee
{
get { return this.employeeBindingSource.DataSource as Employee; }
set
{
this.employeeBindingSource.DataSource = value;
if (value != null)
{
string msg = "Employee ID: " + value.ID;
this.toolTip.SetToolTip(this.employeePicture, msg);
}
}
}
}
There are two things to note in the code above. I had to handle the Format
event of the PictureBox
’s Image
property's Binding
object. That was necessary so that I could convert the Employee
object’s PictureFile
property string
to a Bitmap
instance. If I did not do that, the Image
property binding would fail because it cannot assign a string
to a property of type Image
.
Also, notice that when the Employee
property is set, I give the PictureBox
a tooltip message. The tooltip shows the employee’s ID
, prefixed with "Employee ID:". I tried to set this binding up in the designer, but could not find a way to do it.
Overall, it was very easy to create this simple app in Windows Forms. Most of the job was done in the visual designer, and the rest required small amounts of code. Windows Forms is definitely a great rapid-application-development (RAD) platform.
The WPF Version
I did not have to write a single line of C# code to create this program in WPF. The work involved drag-and-dropping in Cider and then making a few simple edits in the XAML written for me by Cider. If I had a Visual Designer working with me on this, I could have shown him/her the properties of the Employee
class and had him/her do all the work for me. In a sense, I suppose that balances out Cider’s current deficiencies, since I would not have needed to use it in the first place! :)
The WPF version of the demo application has one Window
and a custom UserControl
, called EmployeeControl
. This is the same setup as the WinForms version. Instead of using the WinForms FlowLayoutPanel
to host EmployeeControl
s created in code, the WPF application uses an ItemsControl
to host the user controls. Moreover, the ItemsControl
takes care of creating the EmployeeControl
s for me, so I didn’t need to write a loop that creates them in the Window
’s code-behind.
Here is the Window
’s XAML file (keep in mind that I did not clean up Cider’s XAML, aside from formatting it a little bit):
<Window
x:Class="WpfApp.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp"
xmlns:model="clr-namespace:BusinessObjects;assembly=BusinessObjects"
Title="WPF App" Height="558" Width="503"
WindowStartupLocation="CenterScreen"
>
<Window.DataContext>
<ObjectDataProvider
ObjectType="{x:Type model:Employee}"
MethodName="GetEmployees"
/>
</Window.DataContext>
<Grid>
<Label
Name="label1"
HorizontalContentAlignment="Center" VerticalAlignment="Top"
FontSize="20" FontWeight="Bold"
Height="36.6" Margin="0,16,0,0"
>
Employees:
</Label>
<ItemsControl
ItemsSource="{Binding}"
HorizontalContentAlignment="Center"
Margin="46,59,50,0"
Focusable="False"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:EmployeeControl />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
There are a couple of things to notice about this Window
’s XAML. Its DataContext
is assigned an ObjectDataProvider
object that calls the static GetEmployees
method of my Employee
class. Once the DataContext
is set to that array of Employee
objects, setting the ItemsControl
’s ItemsSource
to “{Binding}” means that the control will display all of those Employee
s.
We don’t need a loop in the code-behind to create EmployeeControl
instances for each Employee
. This is because the ItemsControl
’s ItemTemplate
property is set to a DataTemplate
that will create an EmployeeControl
for each Employee
in the list. Since the ItemsControl
’s ItemsSource
is bound to an array of Employee
objects and its ItemTemplate
knows how to create an EmployeeControl
, there is no reason to write any code like we saw in the WinForms example.
I had to edit the XAML file by hand to create the XML namespace aliases, the ObjectDataProvider
, the ItemsControl
’s ItemTemplate
, and even the ItemsControl
itself. I’m not sure why the Visual Studio Toolbox does not contain ItemsControl
by default. I also had to edit the XAML to include all bindings, since Cider does not allow you to create data bindings on the design surface.
The code-behind for EmployeeControl
is also empty, except for the obligatory call to InitializeComponent
that is automatically written when you create a new UserControl
. Unlike the WinForms app, I was able to create all of the data bindings without writing code (but I did have to create them all in XAML).
Here is the XAML for EmployeeControl
. Please note: I typically avoid showing gobs of uninteresting, poorly formatted XAML in my articles, but I made an exception in this case because I want to give you a realistic impression of the XAML created by Cider in a RAD scenario:
<UserControl x:Class="WpfApp.EmployeeControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="137" Width="372">
<Border
BorderBrush="Black"
BorderThickness="1"
Margin="2"
SnapsToDevicePixels="True">
<Grid Height="129">
<Image Source="{Binding PictureFile}"
Margin="2" Name="image1" Stretch="None"
Width="96" Height="125" HorizontalAlignment="Left" >
<Image.ToolTip>
<TextBlock>
<Run TextBlock.FontWeight="Bold">Employee ID:</Run>
<TextBlock Margin="4,0,0,0" Text="{Binding ID}" />
</TextBlock>
</Image.ToolTip>
</Image>
<Label
Content="{Binding FullName}"
Height="34" Margin="99,2,0,0"
Name="fullNameLabel"
VerticalAlignment="Top"
HorizontalContentAlignment="Right"
FontSize="16" FontWeight="Bold" />
<Label Margin="108,34,0,0" Name="firstNameLabel"
FontWeight="Bold" Height="28"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Width="73">First Name:</Label>
<TextBox
Text="{Binding FirstName}"
HorizontalAlignment="Right" Margin="0,39,10,0"
Name="textBox1" Width="172" Height="23"
VerticalAlignment="Top" TextDecorations="None" />
<Label FontWeight="Bold" Height="28" Margin="108,0,0,34"
Name="lastNameLabel" VerticalAlignment="Bottom"
HorizontalAlignment="Left"
Width="73">Last Name:</Label>
<TextBox
Text="{Binding LastName}"
Height="23" Margin="0,0,10,34" Name="textBox2"
VerticalAlignment="Bottom" HorizontalAlignment="Right"
Width="172" />
<Label Height="28" Margin="108,0,185,2"
Name="startDateLabel" VerticalAlignment="Bottom"
FontWeight="Bold">Start Date:</Label>
<Label
Content="{Binding StartDate}"
Height="28" HorizontalAlignment="Right" Margin="0,0,10,2"
Name="startDateValueLabel" VerticalAlignment="Bottom"
Width="172" />
</Grid>
</Border>
</UserControl>
I needed to edit this XAML file to create the Image
element’s ToolTip
, and to creating bindings for the various bound elements. Other than that, all of the XAML was written by Cider while I drag-and-dropped controls from the Toolbox onto the design surface and adjusted property values. If I had written this XAML by hand, it probably would be half the size.
Note that the Image
’s tooltip contains the same message as the WinForms PictureBox
’s tooltip, but in WPF, it is possible to apply formatting to the tooltip’s text. Since a ToolTip
in WPF can contain any type of content, I was able to put a miniature, lightweight document in there. That allowed my tooltip to match the rest of the UI more closely, which uses bold for header text.
Conclusion
As a programming platform, Windows Forms requires that I write code to do things that WPF makes it trivial to do in markup. As a RAD environment, Windows Forms allows me to get 90% of my UI work done with intuitive wizards and property grids in Visual Studio. WPF requires me to frequently dip into the XAML file and tweak it, since it does not yet have rich design-time support.
For developers with a lot of experience in WinForms, being required to edit markup might seem strange and unnatural. For ASP.NET developers, this seems like second nature because they are already accustomed to working with a markup file and a code-behind file. At the end of the day, most WPFers find editing XAML by hand to be a pleasure!
This article is not meant to sway you toward or away from WPF or WinForms. We barely scratched the surface of those two platforms. If you are trying to decide whether to move to WPF for your next project, hopefully this article will help you make a more informed decision. If you have been wondering what a simple WPF application looks like, compared to an equivalent WinForms app, maybe this article satisfied your curiosity. If you are looking for reasons to explain why WPF rocks or sucks, perhaps I have given you some more ammo. :)
Happy coding!