TX Text Control .NET for WPF

Load and Save Documents

Topic

Users expect an editor to open their files, save their work and warn them before unsaved changes are lost. The WPF TextControl shows its own Open and Save As dialog boxes and also reads and writes files and byte arrays without any dialog. Using the window of your first WPF application, you add a File menu with New, Open, Save, Save As and Exit, keep track of the current file, and ask before a changed document is discarded. The last section moves the same commands into the application menu of a ribbon.

Prerequisites

How Loading and Saving Works

Load replaces the complete contents of the editor with a document, and Save writes the complete document. Both have two kinds of overloads:

  • With a dialog box. Without a file name, Load opens the Open dialog box and Save the Save As dialog box. The StreamType values you pass decide which file format filters the dialog offers. These overloads return a TXTextControl.WPF.DialogResult: OK, Cancel, or None when the user left the dialog with the ESC key.
  • Without a dialog box. With a file path, a stream, a byte array or a string, the methods load and save directly. Save creates the file or overwrites it.

Supported File Formats lists the formats and their StreamType values.

Add the File Menu

Add a Menu as the first child of the DockPanel in MainWindow.xaml, and name the handlers of the TextControl and the window:

<Window x:Class="MyWPFApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:WPF="clr-namespace:TXTextControl.WPF;assembly=TXTextControl.WPF"
        Title="MainWindow" Height="450" Width="800"
        Closing="Window_Closing">
    <DockPanel>
        <Menu DockPanel.Dock="Top">
            <MenuItem Header="_File">
                <MenuItem Header="_New" Click="New_Click" />
                <MenuItem Header="_Open..." Click="Open_Click" />
                <MenuItem Header="_Save" Click="Save_Click" />
                <MenuItem Header="Save _As..." Click="SaveAs_Click" />
                <Separator />
                <MenuItem Header="E_xit" Click="Exit_Click" />
            </MenuItem>
        </Menu>
        <WPF:ButtonBar x:Name="buttonBar1" />
        <WPF:RulerBar x:Name="rulerBar1" />
        <WPF:StatusBar x:Name="statusBar1" />
        <WPF:RulerBar x:Name="rulerBar2" DockPanel.Dock="Left"
                      HorizontalAlignment="Left" VerticalAlignment="Stretch" />
        <WPF:TextControl x:Name="textControl1"
                         ButtonBar="buttonBar1" RulerBar="rulerBar1"
                         StatusBar="statusBar1" VerticalRulerBar="rulerBar2"
                         Loaded="textControl1_Loaded" Changed="textControl1_Changed" />
    </DockPanel>
</Window>

Closing is the WPF event of the window that occurs before it closes, and Changed the event of the TextControl that occurs when the contents of the document change.

Write the Commands

Replace the contents of MainWindow.xaml.cs with the following code:

using System;
using System.ComponentModel;
using System.IO;
using System.Windows;
using TXTextControl;

namespace MyWPFApp
{
    public partial class MainWindow : Window
    {
        // The formats offered in the Open and Save As dialog boxes
        private const StreamType DocumentFormats =
            StreamType.WordprocessingML | StreamType.RichTextFormat |
            StreamType.HTMLFormat | StreamType.AdobePDF;

        private string currentFile;            // null until the document has a file
        private StreamType currentStreamType;
        private bool isModified;

        public MainWindow()
        {
            InitializeComponent();
            UpdateTitle();
        }

        private void textControl1_Loaded(object sender, RoutedEventArgs e)
        {
            textControl1.Focus();
        }

        private void New_Click(object sender, RoutedEventArgs e)
        {
            if (!ConfirmSaveChanges())
            {
                return;
            }

            textControl1.ResetContents();

            currentFile = null;
            isModified = false;
            UpdateTitle();
        }

        private void Open_Click(object sender, RoutedEventArgs e)
        {
            if (!ConfirmSaveChanges())
            {
                return;
            }

            LoadSettings loadSettings = new LoadSettings()
            {
                // Import the merge fields of Word documents as fields
                ApplicationFieldFormat = ApplicationFieldFormat.MSWord
            };

            // Show the Open dialog box. Cancel leaves the document unchanged.
            if (textControl1.Load(DocumentFormats | StreamType.AllFormats, loadSettings)
                != TXTextControl.WPF.DialogResult.OK)
            {
                return;
            }

            currentFile = loadSettings.LoadedFile;
            currentStreamType = loadSettings.LoadedStreamType;
            isModified = false;
            UpdateTitle();
        }

        private void Save_Click(object sender, RoutedEventArgs e)
        {
            SaveDocument();
        }

        private void SaveAs_Click(object sender, RoutedEventArgs e)
        {
            SaveDocumentAs();
        }

        private void Exit_Click(object sender, RoutedEventArgs e)
        {
            // Closing asks about unsaved changes
            Close();
        }

        private bool SaveDocument()
        {
            // A new document has no file yet
            if (currentFile == null)
            {
                return SaveDocumentAs();
            }

            textControl1.Save(currentFile, currentStreamType);

            isModified = false;
            UpdateTitle();
            return true;
        }

        private bool SaveDocumentAs()
        {
            SaveSettings saveSettings = new SaveSettings();

            // Show the Save As dialog box
            if (textControl1.Save(DocumentFormats, saveSettings)
                != TXTextControl.WPF.DialogResult.OK)
            {
                return false;
            }

            currentFile = saveSettings.SavedFile;
            currentStreamType = saveSettings.SavedStreamType;
            isModified = false;
            UpdateTitle();
            return true;
        }

        private void textControl1_Changed(object sender, EventArgs e)
        {
            if (!isModified)
            {
                isModified = true;
                UpdateTitle();
            }
        }

        private void Window_Closing(object sender, CancelEventArgs e)
        {
            if (!ConfirmSaveChanges())
            {
                e.Cancel = true;
            }
        }

        // Returns false if the user canceled
        private bool ConfirmSaveChanges()
        {
            if (!isModified)
            {
                return true;
            }

            MessageBoxResult result = MessageBox.Show(this,
                "Do you want to save the changes to the document?",
                "Save Changes",
                MessageBoxButton.YesNoCancel,
                MessageBoxImage.Question);

            if (result == MessageBoxResult.Cancel)
            {
                return false;
            }

            if (result == MessageBoxResult.Yes)
            {
                return SaveDocument();
            }

            return true;
        }

        private void UpdateTitle()
        {
            string name = currentFile == null ? "Untitled" : Path.GetFileName(currentFile);
            Title = name + (isModified ? "*" : "") + " - My Editor";
        }
    }
}

The code works as follows:

  • New. ResetContents deletes the complete contents of the editor.
  • Open. Load with StreamType values and a LoadSettings object shows the Open dialog box with the filters of these formats. StreamType.AllFormats adds an item that lists every file the dialog can open. After the user has opened a file, LoadedFile and LoadedStreamType contain its path and format.
  • Word fields. ApplicationFieldFormat set to MSWord imports the fields defined by Microsoft Word, such as merge fields, into ApplicationFields.
  • The dialog result. The type is written in full, TXTextControl.WPF.DialogResult, because inside a WPF window the name DialogResult refers to the window's own DialogResult property.
  • Save. A document that has a file is saved to it in its format. A new document goes to Save As.
  • Save As. Save with StreamType values and a SaveSettings object shows the Save As dialog box. SavedFile and SavedStreamType contain the path and format the user chose.
  • Unsaved changes. The Changed handler marks the document as modified and shows an asterisk in the title. ConfirmSaveChanges asks before New, Open or closing the window discards the changes; setting e.Cancel in Window_Closing keeps the window open.

Press F5, type some text and choose File > Save As.... Save the document as DOCX, change it, and choose File > New: the application asks whether to save the changes.

Load and Save Without a Dialog Box

For files your code knows, pass the path and the format:

textControl1.Load("template.docx", StreamType.WordprocessingML);
textControl1.Save("letter.rtf", StreamType.RichTextFormat);

A document from a database or a web service usually arrives as a byte array. Load and Save take it with a BinaryStreamType:

// Load a document from a byte array
byte[] data = File.ReadAllBytes("template.docx");
textControl1.Load(data, BinaryStreamType.WordprocessingML);

// Save the document to a byte array
byte[] docx;
textControl1.Save(out docx, BinaryStreamType.WordprocessingML);

Save creates the array; you declare the variable only. With BinaryStreamType.AdobePDF, the same call returns a PDF document.

Call these methods from an event handler or from the Loaded handler of the TextControl, not from the constructor of the window: the editor is ready for a document only once it has been loaded into the window.

Save Document Properties

A SaveSettings object passed to Save also carries the properties of the document, such as its author and title, which are saved in DOCX and PDF files. Create a PDF sets them step by step. When a document is loaded, DocumentSettings of the TextControl contains its properties.

Add the Commands to the Ribbon

In the ribbon window of Create a WPF Ribbon Application, the commands belong in the application menu, which opens from the button at the left of the tabs. The WPF ribbon defines it with Ribbon.ApplicationMenu; add it as the first element inside the Ribbon:

<Ribbon x:Name="ribbon1" DockPanel.Dock="Top">
    <Ribbon.ApplicationMenu>
        <RibbonApplicationMenu>
            <RibbonApplicationMenuItem Header="New" Click="New_Click" />
            <RibbonApplicationMenuItem Header="Open..." Click="Open_Click" />
            <RibbonApplicationMenuItem Header="Save" Click="Save_Click" />
            <RibbonApplicationMenuItem Header="Save As..." Click="SaveAs_Click" />
            <RibbonApplicationMenuItem Header="Exit" Click="Exit_Click" />
        </RibbonApplicationMenu>
    </Ribbon.ApplicationMenu>
    <WPF:RibbonFormattingTab x:Name="ribbonFormattingTab1" />
    <!-- the other tabs -->
</Ribbon>

The Click handlers are the methods above, unchanged, in the class derived from RibbonWindow. The TX Words sample gives the menu items the icons and texts of TX Text Control with the ResourceProvider class, and adds recent files and a Quick Access Toolbar. Its source code is in %USERPROFILE%\Documents\TX Text Control 34.0.NET for WPF\CSharp\TX Words\Ribbon.

Next Steps