Embed Custom GUIs in WPF

The FlowDocument class in the Windows Presentation Framework—the graphics and user interface engine in .NET 3.0—lets you easily manipulate documents containing rich content.


July 29, 2008
URL:http://drdobbs.com/windows/embed-custom-guis-in-wpf/209800469

Eric has developed everything from data-reduction software for particle bombardment experiments to software for travel agencies. He can be contacted at [email protected].


Windows Presentation Foundation (WPF) is the graphics and user interface engine that debuted in .NET 3.0. WPF's FlowDocument class lets you easily manipulate documents containing rich content. FlowDocuments can be displayed in FlowDocumentReaders and FlowDocumentPageViewers, and are editable, with built-in spellcheck, if they're nested in RichTextBox controls. FlowDocuments can include all the content you'd expect—text, graphics, bullet and numbered lists, tables, and even hyperlinks. Additionally, since FlowDocuments can contain objects derived from the UIElement class, interactive GUIs can be embedded in FlowDocuments—buttons, radio buttons, custom controls, and user controls, for instance. (When embedding GUIs in RichTextBoxes, set the IsDocumentEnabled property to True.)

Unfortunately, WPF 3.5 does not fully support custom GUIs in FlowDocuments. In particular, drag-and-drop, cut-and-paste, deserialization, and printing require extra coding for full-fledged embedded GUI support. In this article, I show how to insert UserControls in FlowDocuments to make deserialization, clipboard operations, and printing work. I've included the full source code for two applications that demonstrate the issues—and workarounds—with embedded GUIs (available online; see "Resource Center," page 5). To use the code, load WPFCompoundDocuments.sln in Visual Studio 2008 or Visual C# 2008 Express Edition, and build and run the User Control Demo program (Figure 1). The User Control Demo illustrates the problems with UserControls in FlowDocuments.

Before pressing the Insert UserControl button, type some text in the RichTextBox on the left. Press Serialize and the document's contents are serialized to a string. Then press Clear and Deserialize, and you see the document's text restored. So far, so good. Now press Insert UserControl, Serialize, and Deserialize. When you press Deserialize, a XamlParseException is thrown, with this message:

[Click image to view at full size]

Figure 1: User Control Demo program.

Cannot set Name attribute value "timestampTextBlock" on element "TextBlock". "TextBlock" is under the scope of element "TimeStampControl", which already had a name registered when it was defined in another scope.

One solution to this problem is to not use a Name attribute for any child element in a UserControl's XAML markup. However, for all but the simplest UserControls, this would require too much effort. For example, the PhotoControl (see PhotoControl.xaml and PhotoControl.xaml.cs) manipulates several XAML elements by name (for instance, slideShowCheckBox, displayImage, imageListBox, and so on). The XAML element names could be removed, and each element could be searched for in the control's XAML markup, but there's a better solution. Keep reading!

Now try to copy-and-paste. Press Clear and press Insert UserControl. A TimeStampControl is inserted. Right-click TimeStampControl and select Copy. Notice that the UserControl's markup does not appear in the selection's markup in the Details window. Press Clear and right-click RichTextBox and select Paste. Oops! No UserControl is pasted because it was filtered out of the selection before it was copied to the clipboard. Fortunately, each of these issues can be handled without too much code. To see the workarounds in action, run the WPF Compound Documents app (Figure 2).

The WPF Compound Documents app can deserialize, copy/paste, and print UserControls. Here's how it works.

[Click image to view at full size]

Figure 2: Workarounds in action.

Serialization/Deserialization

Go to the WPF Compound Documents sample app. Select Edit/Insert PhotoControl. Click on the PhotoControl's Add button and add some JPEG files. Look at the XAML markup in the RichTextBox on the right. You'll see that several of the PhotoControl's XAML elements have Name attributes. But you can save and open the document by selecting File/Save and File|Open without encountering the dreaded XamlParseException. How?

This sample application uses PlaceholderControls, which are minimalist UserControls with no content. PlaceholderControls have the same Name attribute value as the UserControls that they represent. The program avoids the deserialization issue by simply replacing all UserControls with PlaceholderControls before serializing the document, and replacing all PlaceholderControls with UserControls after deserializing. Converting between UserControls and PlaceholderControls is done by the ReplaceControlsWithPlaceholders and ReplacePlaceholdersWithRealControls methods (Listing One). ReplaceControlsWithPlaceholders first calls GetUserControlNames, which scans the specified document fragment and retrieves a list of UserControl Name attribute values in the order in which the UserControls appear in the fragment. Then it uses a regular expression to replace the UserControl markup with PlaceholderControl markup. ReplacePlaceholdersWithRealControls scans through the entire document looking for BlockUIContainers and InlineUIContainers because those elements contain each of the document's PlaceholderControls. If it finds a BlockUIContainer or InlineUIContainer that contains a PlaceholderControl, it extracts the PlaceholderControl. Then it determines the type of UserControl represented by the PlaceholderControl. Finally, it creates the UserControl and loads it with state data so that it displays properly.

// Convert UserControls into PlaceholderControls in the xaml markup.
public static string ReplaceControlsWithPlaceholders(string xaml, 
                              TextPointer start, TextPointer end)
{
  string modifiedMarkup = xaml;
  // Get a list of UserControl Name attribute values in the 
  // order in which they appear in the document.
  List<string> userControlNames = GetUserControlNames(start, end);
  // Search for each Name... 
  foreach (string name in userControlNames)
  {
    foreach (string typeName in ControlTypeNames)
    {
      string enhancedUserControl = 
    string.Format(@"<{0} .*?Name=""{1}"".*?</{0}>", typeName, name);
      string placeHolderControl = 
         string.Format(@"<wpfcd:PlaceholderControl 
             Name=""{0}"" Visibility=""Hidden""/>", 
      Regex enhancedUserControlRegex = new Regex(enhancedUserControl);
      // Replace the UserControl with a Placeholder control having 
      // the same Name as the UserControl.
      modifiedMarkup = enhancedUserControlRegex.Replace(
            modifiedMarkup, placeHolderControl);
    }
  }
  return modifiedMarkup;
}
// Convert all PlaceholderControls in the document with
// the UserControls that they represent.
public static void ReplacePlaceholdersWithRealControls(
        FlowDocument document, RichTextBox richTextBox)
{
  int replacements = 0;
  TextPointer current = document.ContentStart;
  // Scan through the entire document...
  while (current.CompareTo(document.ContentEnd) < 0)
  {
    // UserControls will be nested in BlockUIContainer
    // or InlineUIContainer XAML elements.
    BlockUIContainer blockUIContainer = current.Parent as 
    InlineUIContainer inlineUIContainer = current.Parent as 
    // If we found a BlockUIContainer or InlineUIContainer...
    if (blockUIContainer != null || inlineUIContainer != null)
    {
      PlaceholderControl placeHolderControl;
      if (blockUIContainer != null)
      {
        placeHolderControl = 
           blockUIContainer.Child as PlaceholderControl;
      }
      else
      {
        placeHolderControl = inlineUIContainer.Child as 
      }
      // If we found a PlaceholderControl...
      if (placeHolderControl != null)
      {
        // Determine the type of the UserControl that the 
        // PlaceholderControl represents.
        Type controlType = 
      EnhancedUserControlUtils.GetType(placeHolderControl.Name);
        // Create a new UserControl of the proper type.
        IEnhancedUserControl newControl = 
        EnhancedUserControlUtils.ControlFactory(controlType);
        // Change the name of the new control so that it won't
        // overwrite another control's data.
        newControl.Name = 
        EnhancedUserControlUtils.NameFromGuid(Guid.NewGuid());
        // Retrieve the UserControl's state data.
        ICloneable dataToPersist = 
            (ICloneable)EnhancedUserControlUtils.
                       Load(placeHolderControl.Name);
        // Clone the state data so that the new UserControl
        //  won't overwrite another UserControl's data.
        ICloneable newObject = (ICloneable)dataToPersist.Clone();
        // Save the new UserControl's type and state.
        Globals.CompoundDocument.PersitedTypes[newControl.Name] = 
        Globals.CompoundDocument.PersitedValues[newControl.Name] = 
        // Load the new UserControl with its state data.
        ((IEnhancedUserControl)newControl).Load(newObject);
        // Nest the new UserControl inside the BlockUIContainer
        // or InlineUIContainer.
        if (blockUIContainer != null)
        {
          blockUIContainer.Child = (UserControl)newControl;
        }
        else if (inlineUIContainer != null)
        {
          inlineUIContainer.Child = (UserControl)newControl;
        }
        replacements++;
      }
    }
   current=current.GetNextContextPosition(LogicalDirection.Forward);
  }
  ...
}
Listing One

Clipboard Operations

Go back to the WPF Compound Documents sample application and insert some PhotoControls and TimeStampControls. Select content that includes these controls, right-click the selection, and select Copy. Then verify that the copied content can be pasted. The sample app is able to copy, cut, and paste UserControls by following this procedure when content is copied (see DocumentWindow.Copy). Before copying, the Name attributes of all UserControls in the selected content are extracted by a call to GetUserControlNames (Listing Two). Then PlaceholderControls are inserted into the empty BlockUIContainers and InlineUIContainers. The inserted PlaceholderControls have the same Name attribute value as the original UserControls. This modified XAML, containing the PlaceholderControls, is copied to the clipboard. Later, when users perfom a paste, the pasted PlaceholderControls is replaced with the original UserControls.


// Get control names of all UserControls in the specified document 
// region.
public static List<string> GetUserControlNames(TextPointer start, 
                                               TextPointer end)
{
  List<string> userControlNames = new List<string>();
  TextPointer current = start;
  // Scan through the document fragment...
  while (current.CompareTo(end) < 0)
  {
    // If we encounter something that could potentially
    // contain a UserControl...
    if (current.Parent is BlockUIContainer || 
        current.Parent is InlineUIContainer)
    {
      // Get the BlockUIContainer or InlilneUIContainer's  full 
      // XAML markup.
      string containerMarkup = XamlWriter.Save(current.Parent);
      XmlDocument xmlDocument = new XmlDocument();
      xmlDocument.LoadXml(containerMarkup);

      // Extract the Name attribute from the XAML markup.
      XmlAttribute nameAttribute = 
        xmlDocument.DocumentElement.FirstChild.Attributes["Name"];
      string name = null;
      if (nameAttribute != null && 
          !string.IsNullOrEmpty(nameAttribute.Value))
      {
        name = nameAttribute.Value;
      }
      else
      {
        Debug.Assert(false);
      }
      // Store the UserControl's name in the List, avoiding 
      // duplicates.
      if (!userControlNames.Contains(name))
      {
        userControlNames.Add(name);
      }
    }
    current = 
        current.GetNextContextPosition(LogicalDirection.Forward);
  }
  return userControlNames;
}
Listing Two

IEnhancedUserControl

A UserControl usually has state that must be saved before converting it into a PlaceholderControl. The same state has to be loaded when PlaceholderControl is replaced by the original UserControl. For example, when a PhotoControl is converted to a Placeholder control, the image files selected by users must be saved, along with the Slideshow checkbox state, the width and height, and the number of seconds. That's why the PhotoControl and TimeStampControl classes implement the IEnhancedUserControl interface:


public interface IEnhancedUserControl
{
  void Save(IEnhancedUserControl      enhancedUserControl,
          string name, ICloneable              dataToPersist);
  void Load(ICloneable dataToPersist);
  string GetPrintMarkup();
  string Name { get; set; }
}


Because the sample app's UserControls all implement this interface, the program can save each UserControl's state, and restore it whenever necessary. To see the IEnhancedUserControl interface at work, take a look at EnhancedUserControlUtils.ReplacePlaceholdersWithRealControls (Listing One). Once it has found a PlaceholderControl, it calls the ControlFactory to create the real UserControl. It assigns a unique Name value to this control. Then it loads the control with data that was previously saved by calling the UserControl's Load method. Before the data is loaded into the UserControl, it is cloned. This prevents one UserControl from modifying another UserControl's data.

When the document is printed, the print code calls IEnhancedUserControl.GetPrintMarkup() and renders the XAML returned by that method. This lets a UserControl have a totally different appearance when printed. For example, when the PhotoControl is printed, none of the buttons or checkboxes are displayed. Only the selected image appears on the printed page. The print files, DocumentPaginatorWrapper.cs and Print.cs, are based on code posted in Feng Yuan's blog (see the source code for the URL).

It's easy to add your own custom UserControls. Just make sure your UserControl implements the IEnhancedUserControl interface. Also, add your control to EnhancedUserControlUtils.ControlFactory and EnhancedUserControlUtils.ControlTypeNames.

As of WPF 3.5, the support for embedded GUIs in FlowDocuments is incomplete. This situation will likely improve in subsequent versions of .NET. In the meantime, you can use the techniques in this article to create full-fledged interactive GUIs that can be embedded in FlowDocuments.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.