poniedziałek, 29 listopada 2010

Silverlight printing: Fit to page.

For the last days I was trying to handle printing of Silverlight application content in a user friendly way. As I quickly found out (mainly through reading of other posts), printing in Silverlight is not as sophisticated as most people dreamed.
In short, the philosophy of Silverlight printing is focused on two areas.

  • First one is two take an UIElement from application VisualTree and print it, as it is. The main advantage is that all user action, like selected gridview row, checkbox button... are printed out.
  • The second atitude is to create (recreate) UIElements in code-behind and print it out. The advantege of this is that you can layout your elements in fancy way in order to have nice print. The disadvantege is, that you need to make extra programming work.
My goal was to enable printing in many places of the application with one common printing behavior. Also it was important that all user interaction was preserved. So I decided to go with the first attitude.
The only problem with this attitude is, that all content which do not fit to the print page is truncated.
The solution for this, is to rescale the content with transformation to fit the print page:

double scale = 1;
if (e.PrintableArea.Height < this.Target.ActualHeight)
{
    scale = e.PrintableArea.Height / this.Target.ActualHeight;
}

if (e.PrintableArea.Width < this.Target.ActualWidth && e.PrintableArea.Width / this.Target.ActualWidth < scale)
{
    scale = e.PrintableArea.Width / this.Target.ActualWidth;
}

if (scale < 1)
{
    ScaleTransform scaleTransform = new ScaleTransform();
    scaleTransform.ScaleX = scale;
    scaleTransform.ScaleY = scale;
    this.Target.RenderTransform = scaleTransform;
}

e.PageVisual = this.Target;

This code is placed in PrintPage event handler.
Now the printed UIElement fits to the printed page, but during printing in some situations user can see rescaled printing element, which is not a nice user experience. In order to eliminate this behavior, I have used an extra UIElement that will hide the printed part:

if (this.Target.Parent is Grid)
{
    _parentGrid = (Grid)this.Target.Parent;

    _border = new Border();
    _border.BorderBrush = new SolidColorBrush(Colors.Black);
    _border.BorderThickness = new Thickness(1);
    _border.Background = new SolidColorBrush(Colors.LightGray);
    _border.Child = new TextBlock() { Text = "Printing...", HorizontalAlignment= HorizontalAlignment.Center, VerticalAlignment=VerticalAlignment.Center };
    _parentGrid.Children.Add(_border);
}

This code is placed in BeginPrint event handler.
To simplify the code, I have made the prerequisite, that Target UIElement is hosted on Grid. If not hosted on Grid, then the curtain will not show.
For removing curtain after printing has done some code should be placed in EndPrint event handler.
Now, in order to have a nice, reusable piece of code I have created TargetedTriggerAction.

public class PrintTargetedTrigger : TargetedTriggerAction<FrameworkElement>
{
    protected override void Invoke(object parameter)
    {
        Execute();
    }

    /// <summary>
    /// Executes the print action of the target object.
    /// </summary>
    public void Execute()
    {
        if (this.Target == null)
            throw new NullReferenceException("The target object is null.");

        PrintDocument pd = new PrintDocument();
        pd.PrintPage += new EventHandler<PrintPageEventArgs>(pd_PrintPage);
        pd.BeginPrint += new EventHandler<BeginPrintEventArgs>(pd_BeginPrint);
        pd.EndPrint += new EventHandler<EndPrintEventArgs>(pd_EndPrint);
        pd.Print("");
    }

    ...
}

To initialize printing trigger in xaml:

<Button>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <trigger:PrintTargetedTrigger TargetName="TargetElementName"></trigger:PrintTargetedTrigger>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>

in code behind:

private void Button_Click(object sender, RoutedEventArgs e)
{
    PrintTargetedTrigger printTargetedTrigger = new PrintTargetedTrigger();
    printTargetedTrigger.TargetObject = this.TargetElementName;
    printTargetedTrigger.Execute();
}

Sample solution on my skydrive: