How to move a WPF UIElement from the visual tree t

2019-07-27 01:46发布

问题:

My MVVM application is using on-screen visual objects to render screen content to a printed document. My View has a ContentControl that uses DataTemplate resources to determine what to display, but when I try to add that content to a FixedPage object, I get an ArgumentException with the message:

Specified Visual is already a child of another Visual or the root of a CompositionTarget.

The (simplified) XAML code for the view that handles the printing looks like this:

<DockPanel>
    <!-- Print button -->
    <Button DockPanel.Dock="Top"
            DataContext="{Binding Path=PrintCommand}"
            Click="PrintButton_Click" />
    <!-- Print container. -->
    <Border BorderBrush="Black" BorderThickness="1" DockPanel.Dock="Top" >
        <!-- The visual content of this is what needs adding to the FixedPage.  -->
        <ContentControl x:Name="printContentControl"
                        Content="{Binding Path=PrintMe}" />
    </Border>
</DockPanel>

... and the code behind for the print button click handler that has the problem looks like this:

private void PrintButton_Click(object sender, RoutedEventArgs e)
{
    // Get a reference to the Visual objects to be printed.
    var content = VisualTreeHelper.GetChild(printContentControl, 0);
    Debug.Assert((content as UIElement) != null, "Print content is not a UIElement");

#region This doesn't work.  See below for other things that also don't work.
    printContentControl.Content = null;
    // The line below doesn't help, but tried it in case the framework
    // needed a poke to get it to update the visual tree.
    UpdateLayout();
#endregion

    // Make sure that all the data binding, layout, etc. has completed
    // before the visual object is printed by queuing the print job at
    // a later priority.
    Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(delegate()
    {
        // Create a fixed page to hold the content to print.
        FixedPage page = new FixedPage();

        // Add the content to print to the page.
#region Here be dragons: 'System.ArgumentException'
        page.Children.Add(content as UIElement);
#endregion

        // Send the document back to the View model for actual printing.
        var command = ((sender as Button).DataContext as ICommand);
        command.Execute(page);
    }));
}

I've tried the following things to solve the problem, all with no success:

  • Setting the Content member of the ContentControl to null (as in the code above).
  • Setting this.Content=null. Screen goes blank, but still get the exception.
  • Using RemoveLogicalChild() on various controls. None of which worked.
  • Using RemoveVisualChild() on the printContentControl and the content object. Neither of those were children of the current object.
  • Using DataTriggers in the XAML to replace the template when the Content or DataContext of the ContentControl is `{x:Null}'. No effect.

It doesn't matter if the appearance of the screen is messed up, as the ViewModel changes the View when its print command is executed.

How can I move the printContentControl's visual content (or the entire control) from the View's visual tree to the ViewModel's FixedPage object?

Dirty work-around

By creating a wrapper class for the content to be printed, I can make the functionality work, but it doesn't answer the original question.

Wrapper class

public class PrintContentControl : UserControl
{
    internal UIElement GetPrintContent()
    {
        UIElement printContent = Content as UIElement;
        Content = null;
        return printContent;
    }
}

Updated XAML using the wrapper to hold the printable content

<Border BorderBrush="Black" BorderThickness="1" DockPanel.Dock="Top" >
    <c:PrintContentControl x:Name="printContent">
        <ContentControl ContentTemplate="{StaticResource ReportTemplate}"
                        Content="{Binding Path=PrintMe}" />
    </c:PrintContentControl>
</Border>

Updated code behind to get the printable content from the wrapper

private void PrintButton_Click(object sender, RoutedEventArgs e)
{
    // Use the wrapper to get the Visual objects to be printed.
    UIElement content = printContent.GetPrintContent();
    Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(delegate()
    {
        // Works fine now.
        FixedPage page = new FixedPage();
        page.Children.Add(content as UIElement);
        var command = ((sender as Button).DataContext as ICommand);
        command.Execute(page);
    }));
}