Creating the Same Program in Windows Forms and WPF

本文转自http://www.codeproject.com/KB/WPF/winforms2wpf.aspx

 

Shows how to write the same simple program in Windows Forms and WPF.

 

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:

WinFormsApp.png

Here is a screenshot of the WPF version:

WpfApp.png

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):

Collapse | Copy Code
public class Employee : INotifyPropertyChanged

{

    #region Creation



    public static Employee[] GetEmployees()

    {

        // In a real app this would probably call into a         // data access layer to get records from a database.         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 // Creation 

    #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 // Properties 

    #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:

Collapse | Copy Code
public partial class Form1 : Form

{

    public Form1()

    {

        InitializeComponent();



        // Create and initialize a usercontrol for each employee.         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:

WinForms-databinding-wizard.png

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:

Collapse | Copy Code
/// <summary> /// A WinForms control that displays an Employee object. /// </summary> public partial class EmployeeControl : UserControl

{

    public EmployeeControl()

    {

        InitializeComponent();



        // Convert the picture file path to a Bitmap.         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;



            // The employee's picture shows a tooltip of their ID.             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 EmployeeControls created in code, the WPF application uses an ItemsControl to host the user controls. Moreover, the ItemsControl takes care of creating the EmployeeControls 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):

Collapse | Copy Code
<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 Employees.

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:

Collapse | Copy Code
<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!

License

 

你可能感兴趣的:(windows)