In previous blogs, I looked at how to access a plain text file through a RichTextBox and then how to open and save content in RTF format. In the second blog, I also looked at some core editing features which are available as keyboard shortcuts.
This time I want to look at how to create a simple text editor based around the RichTextBox. I also want to cover the scenario where you load a file of one type, such as a plain text file, make changes and then save it as an RTF file.
Because there is quite a lot of content here, I’ve numbered the sections and listed them:
- Introduction to creating a WPF User Control in a library
- The skeleton of the RTBEditor user control
- Referencing the user control from another project
- Creating the File Menu Item of the user control
- Creating and coding the File >New menu item
- Creating and coding the File > Open menu item.
- Creating and coding the File > Save As Text menu item
- Creating and coding the File > Save As Rtf menu item
- Creating and coding the File > Exit menu item
- Creating the Edit menu item
- Creating the Format and Align menu items
1. Creating a WPF User Control
Instead of adding a RichTextBox directly into a Window, I’m going to create a WPF UserControl. In the interests of reusability and portability, I’ll store this in a WPF User Control Library. To do this from your currently open WPF Solution:
- Select File > Add > New Project in the Visual Studio IDE main menu.
- Select ‘Windows’ from the list of Project types.
- Select WPF User Control Library from the list of templates.
- Rename it. I named the library ‘WPFUserControls’.
- Click OK.
To add a new User Control to the library:
- Select Project > Add New Item from the IDE menu.
- Select WPF from the Categories list.
- Select User Control (WPF) from the Templates list.
- Rename it. I named it RTBEditor.xaml.
- Click ‘Add’.
2. Skeleton of the User Control
To create the skeleton of the User Control, do the following:
- Delete the default Grid in the UserControl markup.
- Replace it with a DockPanel.
- Inside the DockPanel, add a Menu element.
- Set its DockPanel.Dock property to Top.
- Set its MinHeight property to 25.
- Inside the DockPanel, but outside the markup for the Menu, add a RichTextBox.
- Name it ‘RTB’.
- Set its DockPanel.Dock property to Bottom.
- Set its MinHeight and MinWidth properties to 200.
The markup below includes the steps described above, plus a few other less important property settings I’m using for demo purposes:
<DockPanel>
<Menu Margin="0,0,0,5" Name="Menu1" DockPanel.Dock="Top" MinHeight="25" >
</Menu>
<RichTextBox Name="RTB" BorderThickness="2" DockPanel.Dock="Bottom"
MinHeight="200" MinWidth="200"
Background="#FFEDEAEA">
</RichTextBox>
</DockPanel>
3. Referencing the User Control
When I’m building a User Control, I like to insert an instance of it in a Window, so I can see how things are progressing.
Because the User Control is stored in a library, it’s necessary to reference this library from any project that wants to use it. In the Solution I’m using, I have two projects; the Library, named WPFUserControls and a standard WPF project named WpfRichTextBox. You can see this from the screenshot below:
I need to add a reference to the library in the WPF Window Application project so that I can access and use the RTBEditor user control in one or more Windows. To do this:
- Ensure you have the WPF Window Application project selected in the Solution Explorer.
- Select Project > Add Reference form the IDE main menu.
- In the Add Reference dialog box, click on the Projects tab.
- There will only be one Project Name in the list, so ensure this is selected and click OK.
In WPF, another step is needed. You have to add details of the namespace to the markup of the Window where you want to use the user control. I’m using a file named Window2 to host the text editor control, so I need to add the following markup to the xml namespaces at the top of the file:
xmlns:uc="clr-namespace:WPFUserControls;assembly=WPFUserControls"
You can type that in by hand if you want to, but it’s not the easiest syntax to remember. Far easier is to type as far as the “uc=” point and then use the Intellisense list to select the library project.
In this case, you want to select the ‘WPFUserControls in assembly WPFUserControls’ item, because this is the name of the project you are referencing. When you click on it, the correct syntax, as shown in the earlier XAML snippet, will be created for you automatically.
With this in place, you can add an instance of the user control to the Window. Here’s the markup for the Window so far:
1 <Window x:Class="Window2"
2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4 xmlns:uc="clr-namespace:WPFUserControls;assembly=WPFUserControls"
5 Title="Using the Text Editor " Height="500" Width="400">
6
7 <Grid>
8 <uc:RTBEditor />
9
10 </Grid>
11 </Window>
There’s not much to see yet, but we can build up the user control and watch its progress.
4. The File Menu of the User Control
WPF Menus are mainly composed of MenuItems. The two key properties of a MenuItem are its Header (the text the user sees) and its Click property (the link to the event handler for the Click event in the code-behind).
MenuItems can be nested, so in this case several sub items are nested inside the main File menu. The following XAML placed inside the Menu1 Menu will create the standard list of File items:
6 <MenuItem Header="_File" Name="FileMenuItem" >
7 <MenuItem Header="_New" Name="mnuItemNew" Click="mnuItemNew_Click" />
8 <MenuItem Header="_Open" Name="mnuItemOpen" Click="mnuItemOpen_Click" />
9 <MenuItem Header="Save as _Text" Name="mnuItemTxtSave" Click="mnuItemTxtSave_Click" />
10 <MenuItem Header="Save as _RTF" Name="mnuItemRTFSave" Click="mnuItemRTFSave_Click" />
11 <Separator />
12 <MenuItem Header="E_xit" Name="mnuExit" Click="mnuExit_Click" />
13 </MenuItem>
The only points to note here are that you can use accelerator keys, such as Alt & F for the File Menu, or – more usefully – Alt & F & N for File > New. Placing the underscore immediately in front of the letter you want as the hotkey is all that’s needed.
You’ll see that I also added a Separator element between the Exit menu item and the others.
5. The New File Item
For our purposes, we don’t really need to create a new file at this point. What the user wants is a blank RichTextBox in which to enter some content. So we can just the use the Clear method to do this:
Private Sub mnuItemNew_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
RTB.Document.Blocks.Clear()
End Sub
In a production application, you would of course want to build in a feature that asks the user to confirm what is effectively a total deletion of the current content.
6. The Open File Item
WPF currently doesn’t have its own OpenFileDialog, but you can use the old Win32 version. Add the following two Imports statements to the code-behind:
Imports Microsoft.Win32
Imports System.IO
The first is for the OpenFileDialog; the second for the FileStream I’m just about to use to open and read the file.
Here is the code for this menu item:
13 Private Sub mnuItemOpen_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
14 'Let the user select a file
15 Dim ofd As New OpenFileDialog
16 With ofd
17 ' Set preferred file types
18 .Filter = "Text Files(*.txt;*.rtf)|*.txt;*.rtf" & _
19 "| All files (*.*)|*.*"
20
21 If .ShowDialog() = True Then
22 ' Identify the chosen file extension
23 Dim dataformat As String
24 Select .FileName.Substring(.FileName.Length – 3)
25 Case "rtf"
26 dataformat = DataFormats.Rtf
27 Case Else
28 dataformat = DataFormats.Text
29 End Select
30
31 ' Then load the file using the appropriate data format
32 Dim fs As New FileStream(.FileName, FileMode.Open, FileAccess.Read)
33 Using fs
34 'Create a TextRange that comprises the start and end points of the RichTextBox text
35 Dim RTBText As New TextRange(RTB.Document.ContentStart, RTB.Document.ContentEnd)
36 RTBText.Load(fs, dataformat)
37 End Using
38 End If
39 End With
40 End Sub
The OpenFileDialog code is straightforward and is no different from what you do in Windows Forms. The Filter aims to encourage users to select text files that are editable in the RichTextBox, but allows them the freedom to choose others. If they choose anything other than an Rtf file, the data format is set to Text. This will allow actual text format files to be displayed properly; any others will simply display their binary form content.
The file load code is the same as I used in the earlier blogs, except that I use a variable named ‘dataformat’ instead of passing in the actual DataFormats object as the second parameter of the Load method of the TextRange.
7. The Save As Text MenuItem
The code to save the content to a .txt file is very similar to that used in the earlier blog. The only difference is that again I have added a file dialog to give the user the choice of file name and save location. This time, of course, it’s a SaveFileDialog.
42 Private Sub mnuItemTxtSave_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
43 Dim sfd As New SaveFileDialog
44 sfd.Filter = "Text Files(*.txt)|*.txt"
45
46 If sfd.ShowDialog = True Then
47 Dim fs As FileStream = File.OpenWrite(sfd.FileName)
48 Using fs
49 Dim RTBText As New TextRange(RTB.Document.ContentStart, RTB.Document.ContentEnd)
50 RTBText.Save(fs, DataFormats.Text)
51 End Using
52 End If
53 End Sub
8. The Save As RTF Menu Item
It isn’t really necessary to have two separate procedures for the two different formats. The above procedure can easily be edited to identify which of the menu items called it and then switch the Filter of the SaveFileDialog and the DataFormats value accordingly. But for now we’ll just live with a bit of unnecessary duplication:
55 Private Sub mnuItemRTFSave_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
56 Dim sfd As New SaveFileDialog
57 sfd.Filter = "Rtf Files(*.Rtf)|*.Rtf"
58
59 If sfd.ShowDialog = True Then
60 Dim fs As FileStream = File.OpenWrite(sfd.FileName)
61 Using fs
62 Dim RTBText As New TextRange(RTB.Document.ContentStart, RTB.Document.ContentEnd)
63 RTBText.Save(fs, DataFormats.Rtf)
64 End Using
65 End If
66 End Sub
9. The Exit MenuItem
I’ll be honest: I included this out of habit and then realised that this is a user control, not a Window or a Form. So you wouldn’t usually want the Exit menu item to unilaterally close down the parent container. So in this case I’ll use ‘Exit’ as meaning ‘Clear the current content’. In which case, this now familiar Clear method will do nicely:
68 Private Sub mnuExit_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
69 RTB.Document.Blocks.Clear()
70 End Sub
10. The Edit Menu Item
This is another container for nested menu items. For this example, I’ve chosen to use the Cut, Copy and Paste commands. Here’s the markup:
14 <MenuItem Header="_Edit">
15 <MenuItem Header="Cut" Command="Cut"/>
16 <MenuItem Header="Copy" Command="Copy" />
17 <MenuItem Header="Paste" Command="Paste" />
18 <Separator />
19 <MenuItem Header="Undo" Command="Undo" />
20 <MenuItem Header="Redo" Command="Redo" />
21 </MenuItem>
You’ll notice that I haven’t used the underscores with these menu items. I could have, but as you will see if you run this project you already have the standard Ctrl & X, Ctrl & C, etc and I thought that a different set with Alt & whatever would just be confusing.
There are several key things to notice in this markup. The first is that there is no Click property pointing to an event handler in the code-behind. In fact, there is no code-behind needed for this functionality. Because all five of these are built-in Application Commands, selecting any of them will cause that action to take place (maybe – more on this in a minute). So, simply by adding the ‘Command=Undo’ to the markup, everything is in place to ensure that selecting that menu item will automatically cause the user’s last action to be undone.
Note that this is an Application wide command, so it will undo whatever user action the application was last aware of. In other words, if this user control is surrounded by other controls in the Window and you apply an action to one of those other controls and then use the RTBEditor’s Undo command, it will undo the action you took on the other control. It’s an application command, not a RichTextBox one.
Next, these commands are smart. If it’s not appropriate for them to be available, then they won’t be. What I mean by that is if, for example, you haven’t selected any content and so there is nothing on the clipboard, then the Paste command won’t be available. The same logic applies to all these commands. Here’s a screenshot:
As you can see, there is no text in the RichTextBox and nothing has been selected. Therefore the Cut and Copy commands (and their wired up menu items) are disabled. In short, these commands know when they can or cannot execute. (If you’re wondering, Paste is available because I had some other content still on the clipboard – it’s an Application wide command, remember).
Notice also that because these are Application commands with built-in shortcut key combinations, those combos are automatically displayed next to each menu item for you.
11. The Format and Align Menu Items
If you’ve groked the idea of how commands are used here, you’ll have no problem with the next two sets of menu items – Format and Align. Here’s the markup:
22 <MenuItem Header="Format">
23 <MenuItem Header="_Bold" Command="{x:Static EditingCommands.ToggleBold}" />
24 <MenuItem Header="_Italic" Command="{x:Static EditingCommands.ToggleItalic}" />
25 <MenuItem Header="_Underline" Command="{x:Static EditingCommands.ToggleUnderline}" />
26 </MenuItem>
27 <MenuItem Header="Align">
28 <MenuItem Header="Left" Command="{x:Static EditingCommands.AlignLeft}" />
29 <MenuItem Header="Center" Command="{x:Static EditingCommands.AlignCenter}" />
30 <MenuItem Header="Right" Command="{x:Static EditingCommands.AlignRight}" />
31 </MenuItem>
The key difference here (and I’ll admit that it caught me out at first) is that you need to use a markup extension to get at these commands. You can see that the first three commands have the prefix of ‘Toggle’, as in ‘Toggle Bold’, and so on. And the commands do work in exactly that way. Click them once to toggle the effect on, click again to toggle it off. You can select text and apply one of the formats or you can toggle a setting and then begin to type. The setting you have just made will then be applied. That’s why you will find that they are not disabled even when nothing is selected.
You’ll see that the markup extension requires the fully qualified name of the command. There are other groups of commands, other than ApplicationCommands and EditingCommands and I will be looking at some of those in the next blog. I also plan to include other commands and features, such as the ability to colour text and tweak the fonts. At some stage I also want to bring the whole thing completely into the world of WPF by using a FlowDocument as the content container inside the RichTextBox.
Summary
In the meantime, you have the makings of a useful text editor here. There are many other Application and Editing commands and you may well want to add some of those to the menu. WPF’s Commands can make many of these common tasks very easy to implement.