Getting Started with Windows Presentation Foundation (WPF) in .NET Core

August 26, 2020
Written by
Jeff Rosenthal
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
AJ Saulsberry
Contributor
Opinions expressed by Twilio contributors are their own

getting-started-wpf.png

Windows Presentation Foundation (WPF) is a user interface framework for building desktop applications. Implementing Extensible Application Markup Language (XAML) to define and script views, it has become the standard for Windows application user interfaces. Introduced in 2006, Microsoft has reenergized its commitment to WPF with its inclusion into .NET Core.

The tutorial in this post will guide you through creating a project that will mimic many of the basic functions of a word processor. You will be surprised at how easy it is to implement many features with standard tool elements. You will be introduced to some of the many constructs of WPF and create a foundation for experimenting further.

Prerequisites

You’ll need the following tools and resources to build and run this project:

Windows 10 – It puts the Windows in Windows Presentation Foundation.

.NET Core SDK 3.1 – The SDK includes the APIs, runtime, and CLI.

Visual Studio 2019 with the following workloads and individual components:

  • .NET desktop development workload (Includes C#)
  • GitHub Extension for Visual Studio (If you want to clone the companion repository.)

You should have a general knowledge of Visual Studio and the C# language syntax. You will be adding and editing files, and debugging code.

There is a companion repository for this post available on GitHub. It contains the complete source code for the tutorial project.

Creating the project

Begin this tutorial by creating a WPF App (.NET Core) project for C# named WPFTwilioExample. You can put the solution and project folders wherever it’s most convenient for you.

While you could easily use this code in a .NET Framework project, this example uses .NET Core, which is closer to the forthcoming .NET 5 release scheduled for November 2020.

After the tooling finishes creating the project, note the presence of the App.xaml file and its associated App.xaml.cs code-behind file. Also examine the MainWindow.xaml and associated code-behind file, MainWindow.xaml.cs.

MainWindow will be the main window for this application and where you will add UI elements. The App class is the entry point to the application and where the MainWindow.xaml is specified as the startup window. You can see this App.xaml file with the line:

StartupUri="MainWindow.xaml"

If you ever wanted your application to start with another window, this is where you would specify that. For now, leave it as it is.

In the MainWindow.xaml file, note how the main element is a Grid. The Grid is one of several very powerful container components in WPF. Others include StackPanel, Canvas, WrapPanel, UniformGrid and DockPanel. Each has its advantages and situations that it fits best. In this example you will be using the DockPanel.

Replace the existing contents of the MainWindow.xaml file with the following XAML markup:

<Window x:Class="WPFTwilioExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPFTwilioExample"
        mc:Ignorable="d"
        Title="Mini WordProcessor" Height="450" Width="800">
    <DockPanel LastChildFill="True">

    </DockPanel>
</Window>

The changed value for the Title attribute will be displayed in the window’s title bar when the application runs. Also, note that the DockPanel element has an attribute of LastChildFill="True". While this is the default value, it is done for clarity.

The DockPanel is a container that allows you to dock child controls to the top, bottom, left or right. By default, the last control, if not given a specific dock position, will fill the remaining space. Use the DockPanel whenever you need to dock one or several controls to one of the sides, like for dividing up the window into specific areas. In this example, you will be adding a menu, toolbar, and status elements.

With the window ready you will add components to it which will add capability. The first component you will add is applicable to the window as a whole and will be placed outside of the DockPanel. This will be the CommandBindings section where CommandBinding elements  will be defined.

Insert the following XAML markup in the MainWindow.xaml file above the DockPanel element:

    <Window.CommandBindings>
<CommandBinding Command="New" Executed="New_Executed" CanExecute="CommonCommandBinding_CanExecute" />
  <CommandBinding Command="Open" Executed="Open_Executed" CanExecute="CommonCommandBinding_CanExecute" />
        <CommandBinding Command="Save" Executed="Save_Executed" CanExecute="CommonCommandBinding_CanExecute" />
    </Window.CommandBindings>

Open the MainWindow.xaml.cs code-behind file and insert the following C# code after the MainWindow() constructor:

private void CommonCommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
        e.CanExecute = true;
}
private void New_Executed(object sender, ExecutedRoutedEventArgs e)
{
}

private void Open_Executed(object sender, ExecutedRoutedEventArgs e)
{
}

private void Save_Executed(object sender, ExecutedRoutedEventArgs e)
{
}

The code block above adds a group of Commands. Commands are constructs that associate a name with an action. They lend to a more loose coupling between UI elements and the code. In this case, you are adding the New, Open, and Save commands. The Executed attributes in the <CommandBinding> elements in the MainWindow.xaml file direct the Command to execute the listed method while the CanExecute attribute points to the CommonCommandBinding_CanExecute method. The CanExecute associated method in the code-behind file is run when the app is idle to indicate whether the command can be run or it is disabled. In this case it always returns true. This is useful in situations where a command might be disabled unless certain conditions are met.

The _Execute handler methods for the New, Open and Save commands in the code behind are provided as stubs for now and will be fleshed out shortly.

You will now be adding the first UI element to the window. Just inside the DockPanel container  add a menu with File and Edit top level items by inserting the following XAML markup inside the <DockPanel> element:

<Menu DockPanel.Dock="Top">
    <MenuItem Header="_File">
        <MenuItem Command="New" Header="_New" />
        <MenuItem Command="Open" Header="_Open" />
        <MenuItem Command="Save" Header="_Save" />
        <Separator />
        <MenuItem Header="_Exit" />
    </MenuItem>
    <MenuItem Header="_Edit">
        <MenuItem Command="Cut"   Header="Cut    (Ctrl+X)" />
        <MenuItem Command="Copy"  Header="Copy   (Ctrl+C)" />
        <MenuItem Command="Paste" Header="Paste  (Ctrl+V)" />
    </MenuItem>
</Menu>

Note how the _File MenuItem elements reference the Commands that were added earlier.

The _Edit menu items reference Cut, Copy and Paste, but those CommandBindings were not created. The commands reference the ApplicationCommands.Cut, ApplicationCommands.Copy and ApplicationCommands.Paste commands. These are RoutedCommands and will be processed by the TextBox that you will add shortly. Rather than implement these commands in the code, they’ll use the default behavior of the TextBox element. The application will simply route the commands until an element that can handle them processes them. WPF event routing is a very powerful capability. Routed Events tunnel down the element tree and bubble up the element tree.

But while the menu is now visible, the application still does nothing.

Before going on, look at the design panel and notice how the menu bar that was just added with the <Menu> element is positioned at the top of the DockPanel. That results from the DocPanel.Doc="Top" attribute for the Menu element.

Continuing with the UI elements, add the following code to the MainWindow.xaml file inside the DockPanel element and after the menu elements you previously inserted:

<ToolBarTray DockPanel.Dock="Top" Background="LightGray">
    <ToolBar>
        <Button Command="Cut" Content="Cut" />
        <Button Command="Copy" Content="Copy" />
        <Button Command="Paste" Content="Paste" />
        <ToggleButton Command="EditingCommands.ToggleBold" Name="btnBold" Content="Bold"/>
        <ToggleButton Command="EditingCommands.ToggleItalic" Name="btnItalic" Content="Italic"/>
        <ComboBox Name="cmbFontFamily" Width="150" SelectionChanged="cmbFontFamily_SelectionChanged" />
        <ComboBox Name="cmbFontSize" Width="50" IsEditable="True" TextBoxBase.TextChanged="cmbFontSize_TextChanged" />
    
  </ToolBar>
</ToolBarTray>
<StatusBar Name="statusbar" DockPanel.Dock="Bottom">
    <StackPanel Orientation="Horizontal">
        <TextBlock Name="StatusBlock"></TextBlock>
    </StackPanel>
</StatusBar>
<RichTextBox  AcceptsReturn="True" Name="MyTextBox"
    SelectionChanged ="MyTextBox_SelectionChanged">
</RichTextBox>

Here you added a ToolBarTray with several buttons docked to the top, a StatusBar with a TextBlock docked to the bottom, and a RichTextBox as the last element in the DockPanel. Since the RichTextBox is the last element in the DockPanel, it will fill the center of the window.

That takes care of the code in the .xaml file.

You will next add the functionality in the code-behind file MainWindow.xaml.cs which will make the application come alive.

File system dialog boxes use resources from the Microsoft.Win32 and System.IO namespaces, so you will need to add the following using directives to the others in MainWindow.xaml.cs:

using System.IO;
using Microsoft.Win32;

Earlier, you added placeholder stub methods for the New, Open and Save commands. Replace the stub methods with the following code:

private void New_Executed(object sender, ExecutedRoutedEventArgs e)
{
MyTextBox.Document.Blocks.Clear();
}

private void Open_Executed(object sender, ExecutedRoutedEventArgs e)
{
OpenFileDialog dlg = new OpenFileDialog();
       dlg.Filter = "Rich Text Format (*.rtf)|*.rtf|All files (*.*)|*.*";
       if (dlg.ShowDialog() == true)
       {
               FileStream fileStream = new FileStream(dlg.FileName, FileMode.Open);
             TextRange range = new TextRange(MyTextBox.Document.ContentStart, MyTextBox.Document.ContentEnd);
             range.Load(fileStream, DataFormats.Rtf);
        }
 }

 private void Save_Executed(object sender, ExecutedRoutedEventArgs e)
 {
       SaveFileDialog dlg = new SaveFileDialog();
       dlg.Filter = "Rich Text Format (*.rtf)|*.rtf|All files (*.*)|*.*";
       if (dlg.ShowDialog() == true)
       {
           FileStream fileStream = new FileStream(dlg.FileName, FileMode.Create);
           TextRange range = new TextRange(MyTextBox.Document.ContentStart, MyTextBox.Document.ContentEnd);
           range.Save(fileStream, DataFormats.Rtf);
       }
}

New_Executed() is the handler method for the New command. When a new file is to be created the currently displayed text area will be cleared.

The Open_Executed() and Save_Executed() methods are a bit more involved but similar to each other. A dialog box is opened to specify a file to load or save. The file extension applied to the files will be “.rtf” for Rich Text Format.

There are three more things that need to be added to wrap up the functionality:

  1. Populating the combo boxes
  2. Taking action when the combo boxes selection changes
  3. Providing visual feedback for selected text

The combo boxes for the font and the font size in the toolbar need to be populated. That will be done in the MainWindow class constructor with the following code.

public MainWindow()
{
InitializeComponent();
      cmbFontFamily.ItemsSource = Fonts.SystemFontFamilies.OrderBy(f => f.Source);
      cmbFontSize.ItemsSource = new List<double>() { 8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72 };

}

Although the combo boxes have data to display, they still do not do anything. Add the following code after the MainWindow constructor:

private void cmbFontFamily_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (cmbFontFamily.SelectedItem != null)
MyTextBox.Selection.ApplyPropertyValue(Inline.FontFamilyProperty,   cmbFontFamily.SelectedItem);
}

private void cmbFontSize_TextChanged(object sender, TextChangedEventArgs e)
{
MyTextBox.Selection.ApplyPropertyValue(Inline.FontSizeProperty, cmbFontSize.Text);

}

These methods will be invoked when the combo box selections change. They’ll apply the font type and size styling to text that’s selected.

With that completed there is one block of code left to add, the TextBox.SelectionChanged event handler. This method gets called any time there is a change to the selection in the textbox. It is here that all the elements you added (combo boxes, toolbar buttons, statusbar, and the textbox) are synchronized.

Add the following code to the MainWindow.xaml.cs file at the bottom of the MainWindow class:

private void MyTextBox_SelectionChanged(object sender, RoutedEventArgs e)
{
object temp = MyTextBox.Selection.GetPropertyValue(Inline.FontFamilyProperty);
       cmbFontFamily.SelectedItem = temp;

       temp = MyTextBox.Selection.GetPropertyValue(Inline.FontSizeProperty);
       if(temp is Double)
            cmbFontSize.Text = temp.ToString();

       temp = MyTextBox.Selection.GetPropertyValue(Inline.FontWeightProperty);
       btnBold.IsChecked = (temp != DependencyProperty.UnsetValue) && (temp.Equals(FontWeights.Bold));
            
       temp = MyTextBox.Selection.GetPropertyValue(Inline.FontStyleProperty);
       btnItalic.IsChecked = (temp != DependencyProperty.UnsetValue) && (temp.Equals(FontStyles.Italic));
            
       string info = string.Format($"{MyTextBox.Selection.Text}");
       StatusBlock.Text = info;
 }

This is an interesting block of code. The first thing done is to get the FontFamilyProperty value from the selected code in the textbox. That value is used to select the related item in the cmbFontFamily combo box. In this way, the font of the selected code will be selected in the combo box.

The next several lines do the same thing for the font size, font weight, and bold and italic buttons.

Lastly, the status bar is given something to display and shows the selected text.

Note the type checking done in the if statement prior to setting the cmbFontSize text. If multiple font sizes are detected in the selected text, the value of temp will be a data type that’s incompatible with the data type of  cmbFontSize.Text. Checking for the correct data type prevents an error when assigning the property value.

Testing the completed application

Build and run the application. Enter some text in the textbox that fills the center of the app. Select a portion and make it bold or italic. Change the font size and style. When ready, save the file and give it a name. Creating a new file will clear the textbox for something new. Load the file you just saved and you will be able to continue working on it. Note how the selected text attributes are reflected in the toolbar elements. Observe the status bar when text is selected. You should see the selected text.

Potential enhancements

The RichTextBox control has a lot of very powerful editing capabilities. Exposing those capabilities through commands and a handful of UI elements creates a simple, yet powerful text editor.

The layout created here is a standard style window with menu, toolbar, and status bar and is a good foundation for building additional functionality. Consider adding alignment tools to align text to the left or right or centered. Perhaps you wish to add color to your text in terms of a foreground or background color. The status bar could also be expanded to show more pertinent information, such as the number of words in the text box, or even a clock.

Summary

Windows Presentation Foundation is a very powerful tool. It supports layered text, animations, a powerful event and command routing system, and even includes support for elaborate graphics and styling. Microsoft has committed to the future of WPF with its inclusion in .NET Core and .NET 5 due for release later this year. Give it a try. You may be pleasantly surprised.

Additional resources

The following reference resources provide in-depth information on the topics discussed in this post:

Differences in WPF - Differences between Windows Presentation Foundation (WPF) on .NET Core and .NET Framework. WPF for .NET Core.

RichTextBox Overview - Differences between Textbox and RichTextBox

If you’d like to learn how to build a WPF application to verify phone numbers and obtain information about callers and their phones, check out this post:

Using Twilio Lookup in .NET Core WPF Applications

TwilioQuest – If you’d like to learn more about programming C# and other languages, try this action-adventure game inspired by the 16-bit golden era of computer gaming.

Jeffrey Rosenthal is a C/C++/C# developer and enjoys the architectural aspects of coding and software development. Jeff is a MCSD and has operated his own company, The Coding Pit since 2008. When not coding, Jeff enjoys his home projects, rescuing dogs, and flying his drone. Jeff is available for consulting on various technologies and can be reached via email, Twitter, or LinkedIn.