【.NET 5】【WPF】Edit and print PDF files 2
Tags: dotnet, csharp, wpf
Published: 2021-04-07 11:22:07

## Intro
This time, I will try setting printer properties and printing PDF files what have multiple pages.

* [【.NET 5】【WPF】Edit and print PDF files 1](https://dev.to/masanori_msl/net-5-wpf-edit-and-print-pdf-files-1-4gin)
* [【.NET 5】【WPF】Try MVVM](https://dev.to/masanori_msl/net-5-wpf-try-mvvm-3m0e)

### Environments
* .NET ver.5.0.103
* Microsoft.Extensions.DependencyInjection ver.5.0.1
* Microsoft.Xaml.Behaviors.Wpf ver.1.1.31

## Data Bindings
I will implement these functions.

1. Add one textbox, two buttons, and one combobox.
2. The first button opens a dialog to select a PDF file for printing.
3. After selecting a PDF file, the file path will be set into the textbox.
4. The combobox has printer names what are installed in the PC.
5. The second button starts printing.
6. After the file and the printer selected, the second button will be enabled.

### 1. Add one textbox, two buttons, and one combobox
#### MainWindow.xaml
```xml
<Window x:Class="PrintSample.Main.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:Behaviors="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:vm="clr-namespace:PrintSample.Main"
        xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
        xmlns:local="clr-namespace:PrintSample.Main"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Button x:Name="OpenFileButton" Content="Open" HorizontalAlignment="Left" Height="32" 
            Margin="586,48,0,0" VerticalAlignment="Top" Width="80" FontSize="18"
            Command="{Binding Open}"/>
        <ComboBox x:Name="PrinterSelector" HorizontalAlignment="Left" Margin="37,128,0,0" 
                VerticalAlignment="Top" Width="629" Height="32" FontSize="18" />
        <TextBox x:Name="LoadFileInput" HorizontalAlignment="Left" Margin="220,48,0,0" 
                TextWrapping="NoWrap" VerticalAlignment="Top" 
                Width="320" Height="32" FontSize="18" IsReadOnly="True" IsUndoEnabled="False"/>
        <Button x:Name="PrintButton" Content="Print" HorizontalAlignment="Left" Height="32" 
            Margin="688,368,0,0" VerticalAlignment="Top" Width="80" FontSize="18"
            Command="{Binding Print}"/>
    </Grid>
</Window>
```
#### OpenDialogCommand.cs
```cs
using System;
using System.Windows.Input;
namespace PrintSample.Main.Commands
{
    public class OpenDialogCommand: ICommand
    {
        public event EventHandler? CanExecuteChanged;
        public Action? Action;
        public bool CanExecute(object? parameter)
        {
            return true;
        }
        public void Execute(object? parameter)
        {
            Action?.Invoke();
        }
    }
}
```
#### PrintCommand.cs
```cs
using System;
using System.Windows.Input;
namespace PrintSample.Main.Commands
{
    public class PrintCommand: ICommand
    {
        public event EventHandler? CanExecuteChanged;
        public Action? Action;        
        public bool CanExecute(object? parameter)
        {
            // TODO: Set enabled after selecting a file and a printer
            return true;
        }
        public void Execute(object? parameter)
        {
            Action?.Invoke();
        }
    }
}
```
#### MainViewModel.cs
```cs
using System;
using PrintSample.Files;
using PrintSample.Main.Commands;
using PrintSample.Main.Properties;
using PrintSample.Pdf;
using PrintSample.Prints;
namespace PrintSample.Main
{
    public class MainViewModel
    {
        public OpenDialogCommand Open { get; }
        public PrintCommand Print { get; }
        private readonly FileSelector fileSelector;
        public MainViewModel()
        {
            this.Open = new OpenDialogCommand();
            this.Print = new PrintCommand();
            this.fileSelector = new FileSelector();
            this.Print.Action += async () =>
            {
                // TODO: Print selected PDF
            };
            this.Open.Action += () =>
            {
                // Open file dialog
                fileSelector.Open();
            };
        }
    }
}
```
### 2. The first button opens a dialog to select a PDF file for printing
I used "OpenFileDialog" to show a file dialog.

#### FileSelector.cs
```cs
using System;
using Microsoft.Win32;
namespace PrintSample.Main
{
    public class FileSelector
    {
        public Action<string>? FileSelected;
        public void Open()
        {
            var dialog = new OpenFileDialog();
            dialog.Title = "Open File";
            dialog.Filter = "PDF(*.pdf)|*.pdf";
            if (dialog.ShowDialog() == true)
            {
                // Send the selected file path.
                FileSelected?.Invoke(dialog.FileName);
            }
        }
    }
}
```
#### Result
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qb70e1xt578s1crqszlg.jpg)

* [OpenFileDialog Class (System.Windows.Forms) | Microsoft Docs](https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.openfiledialog?view=net-5.0)

### 3. After selecting a PDF file, the file path will be set into the textbox
I added a data binding into the textbox.

#### MainWindow.xaml
```xml
...
        <TextBox x:Name="LoadFileInput" HorizontalAlignment="Left" Margin="220,48,0,0" 
                Text="{Binding Path=SelectedFile.FilePath, UpdateSourceTrigger=PropertyChanged}"
                TextWrapping="NoWrap" VerticalAlignment="Top"
                Width="320" Height="32" FontSize="18" IsReadOnly="True" IsUndoEnabled="False"/>
...
```
#### SelectedFile.cs
```cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace PrintSample.Main.Properties
{
    public class SelectedFile: INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;
        private string filePath = "";
        public string FilePath
        {
            get
            {
                return this.filePath;
            }
            set
            {
                this.filePath = value;
                OnPropertyChanged("FilePath");
            }
        }
        protected void OnPropertyChanged([CallerMemberName] string? name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }
}
```
#### MainViewModel.cs
```cs
...
    public class MainViewModel
    {
...
        public SelectedFile SelectedFile { get; set; }
        public MainViewModel()
        {
...
            this.SelectedFile = new SelectedFile();
            this.fileSelector.FileSelected += (filePath) =>
            {
                this.SelectedFile.FilePath = filePath;
            };
...
```
* [How to: Control When the TextBox Text Updates the Source - WPF .NET Framework | Microsoft Docs](https://docs.microsoft.com/en-us/dotnet/desktop/wpf/data/how-to-control-when-the-textbox-text-updates-the-source?view=netframeworkdesktop-4.8)
* [How to: Implement Property Change Notification - WPF .NET Framework | Microsoft Docs](https://docs.microsoft.com/en-us/dotnet/desktop/wpf/data/how-to-implement-property-change-notification?view=netframeworkdesktop-4.8)

#### Property classes
The property name and the argument of "OnPropertyChanged" must be same.
And I can add two or more properties in a class.

I can change the data type from "string".
I can convert the type in the setter like below.

```cs
private double width = 0d;
public string Width
{
    get
    {
        return this.width.ToString();
    }
    set
    {
        if(string.IsNullOrEmtpy(value) ||
            double.TryParse(value, out var width) == false)
        {
            this.width = 0d;
        }
        else
        {
            this.width = width;
        }
        OnPropertyChanged("Width");
    }
}
```
### 4. The combobox has printer names what are installed in the PC
#### MainWindow.xaml
```xml
...
        <ComboBox x:Name="PrinterSelector" HorizontalAlignment="Left" Margin="37,128,0,0" 
                VerticalAlignment="Top" Width="629" Height="32" FontSize="18"
                ItemsSource="{Binding SelectedPrinter.PrinterNames, Mode=OneWay}"
                SelectedItem="{Binding SelectedPrinter.PrinterName}"/>
...
```
#### SelectedPrinter.cs
```cs
using System.ComponentModel;
using System.Linq;
using System.Printing;
using System.Runtime.CompilerServices;

namespace PrintSample.Main.Properties
{
    public class SelectedPrinter: INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;
        private readonly string[] printerNames;
        public string[] PrinterNames => this.printerNames;
        private string printerName = "";
        public string PrinterName
        {
            get
            {
                return this.printerName;
            }
            set
            {
                this.printerName = value;
                OnPropertyChanged("PrinterName");
            }
        }
        public SelectedPrinter()
        {
            LocalPrintServer printServer = new LocalPrintServer();
            printerNames = printServer.GetPrintQueues()
                .Select(q => q.Name)
                .ToArray();
        }
        protected void OnPropertyChanged([CallerMemberName] string? name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }
}
```

For data binding, the "ItemsSource" must be a property.
If I change "PrinterNames" like below, the combobox will be empty.
```cs
public string[] PrinterNames()
{
    return this.printerNames;
}
```
### 6. After the file and the printer selected, the second button will be enabled.
#### PrintCommand.cs
```cs
...
    public class PrintCommand: ICommand
    {
        public event EventHandler? CanExecuteChanged;
        public Action? Action;
        private bool actionEnabled = false;
        public bool CanExecute(object? parameter)
        {
            return this.actionEnabled;
        }
...
        public void CheckUpdatingInput(string? filePath, string? printer)
        {
            var newValue = (string.IsNullOrEmpty(filePath) == false &&
                    File.Exists(filePath) &&
                    string.IsNullOrEmpty(printer) == false);
            var statusChanged = (newValue != this.actionEnabled);
            this.actionEnabled = newValue;
            if(statusChanged)
            {
                CanExecuteChanged?.Invoke(this, EventArgs.Empty);
            }
        }
    }
}
```
#### MainViewModel.cs
```cs
...
        public MainViewModel()
        {
...
            this.SelectedFile.PropertyChanged += (_, _) => {
                this.Print.CheckUpdatingInput(this.SelectedFile.FilePath, this.SelectedPrinter.PrinterName);
            };
            this.SelectedPrinter.PropertyChanged += (_, _) => {
                this.Print.CheckUpdatingInput(this.SelectedFile.FilePath, this.SelectedPrinter.PrinterName);
            };
        }
...
```
## Print PDF files what have multiple pages
### Convert from PDF to list of "Image"
I write a method to convert from PDF to list of "Image".

#### PdfLoader.cs (Failed)
```cs
...
        public async Task<List<Image>> LoadAsync(LoadPdfFileArgs args)
        {
            using(FileStream fileStream = new FileStream(args.FilePath, FileMode.Open))
            using(IRandomAccessStream stream = fileStream.AsRandomAccessStream())
            {
                var results = new List<Image>();
                PdfDocument document = await PdfDocument.LoadFromStreamAsync(stream);
                for(uint i = 0; i < document.PageCount; i++)
                {
                    using(PdfPage page = document.GetPage(i))
                    using(MemoryStream memoryStream = new MemoryStream())
                    using(IRandomAccessStream outputStream = memoryStream.AsRandomAccessStream())
                    {
                        await page.RenderToStreamAsync(outputStream);
                        BitmapFrame? bitmap = BitmapDecoder.Create(memoryStream, BitmapCreateOptions.None,    
                                BitmapCacheOption.OnLoad).Frames[0];
                        results.Add(new Image
                        {
                            Source = bitmap,
                        });
                    }
                }
                return results;
            }            
        }
...
```
When I used a single page file, I didn't get any problems.
But when the file had two or more pages, I got an exception.

```
Specified cast is not valid.
```

So I changed the methods to load the file.

#### PdfLoader.cs
```cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
using PrintSample.Files;
using Windows.Data.Pdf;
using Windows.Storage;
using Windows.Storage.Streams;
namespace PrintSample.Pdf
{
    public class PdfLoader: IPdfLoader
    {
        public async Task<List<Image>> LoadAsync(LoadPdfFileArgs args)
        {
            StorageFile file = await StorageFile.GetFileFromPathAsync(args.FilePath);
            using(IRandomAccessStream stream = await file.OpenAsync(FileAccessMode.Read))
            {
                var results = new List<Image>();
                PdfDocument document = await PdfDocument.LoadFromStreamAsync(stream);
                for(uint i = 0; i < document.PageCount; i++)
                {
                    using(PdfPage page = document.GetPage(i))
                    using(IRandomAccessStream outputStream = new InMemoryRandomAccessStream())
                    {
                        await page.RenderToStreamAsync(outputStream);
                        BitmapFrame? bitmap = BitmapDecoder.Create(outputStream.AsStream(),
                                BitmapCreateOptions.None,    
                                BitmapCacheOption.OnLoad).Frames[0];
                        results.Add(new Image
                        {
                            Source = bitmap,
                        });
                    }
                }
                return results;
            }            
        }
    }
}
```
### Print multiple pages
I can add created "Image" into "FixedDocument" to print multiple pages.

#### Printer.cs
```cs
using System.Collections.Generic;
using System.Printing;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Xps;
namespace PrintSample.Prints
{
    public class Printer: IPrinter
    {
        public void Print(List<Image> targets, string printer)
        {
            LocalPrintServer printServer = new LocalPrintServer();
            PrintQueue queue = printServer.GetPrintQueue(printer);
            PrintTicket ticket = queue.DefaultPrintTicket;
            ticket.PageMediaSize = new PageMediaSize(PageMediaSizeName.ISOA4);
            ticket.CopyCount = 1;
            ticket.PageResolution = new PageResolution(72, 72);
            ticket.PageOrientation = PageOrientation.Landscape;
            ticket.PageBorderless = PageBorderless.Borderless;
            var document = GenerateDocument(targets);
            
            XpsDocumentWriter writer = PrintQueue.CreateXpsDocumentWriter(queue); 
            writer.Write(document, ticket);
        }
        private FixedDocument GenerateDocument(List<Image> targets)
        {
            FixedDocument result = new FixedDocument();
            // add list of "Image" as pages
            foreach(var target in targets)
            {
                var page = new FixedPage();
                page.Children.Add(target);
                PageContent content = new PageContent();
                content.Child = page;
                result.Pages.Add(content);
            }
            return result;
        }
    }
}
```
#### MainViewModel.cs
```cs
...
        private readonly IPdfLoader pdfLoader;
        private readonly IPrinter printer;

        public MainViewModel(IPdfLoader pdfLoader,
            IPrinter printer)
        {
...
            this.pdfLoader = pdfLoader;
            this.printer = printer;
            this.Print.Action += async () =>
            {
                var loadedImages = await this.pdfLoader.LoadAsync(
                    new LoadPdfFileArgs(this.SelectedFile.FilePath));
                this.printer.Print(loadedImages);
            };
        }
...
```
* [WPFでの印刷の基本(2) 複数ページの印刷 - Qiita](https://qiita.com/tricogimmick/items/116859965d896e4cc21f)

### Print same images?
The result like below.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zhkms4oqqj68b5wq3hm4.png)

All pages has the same image.

I added outputting bitmap images to check.
But they didn't any problems.

#### PdfLoader.cs
```cs
...
        public async Task<List<Image>> LoadAsync(LoadPdfFileArgs args)
        {
            StorageFile file = await StorageFile.GetFileFromPathAsync(args.FilePath);
            using(IRandomAccessStream stream = await file.OpenAsync(FileAccessMode.Read))
            {
                var results = new List<Image>();
                PdfDocument document = await PdfDocument.LoadFromStreamAsync(stream);
                for(uint i = 0; i < document.PageCount; i++)
                {
                    using(PdfPage page = document.GetPage(i))
                    using(IRandomAccessStream outputStream = new InMemoryRandomAccessStream())
                    {
                        await page.RenderToStreamAsync(outputStream);
                        // Get bitmap from stream
                        BitmapFrame? bitmap = BitmapDecoder.Create(outputStream.AsStream(),
                                BitmapCreateOptions.None,    
                                BitmapCacheOption.OnLoad).Frames[0];
                        // for debugging
                        SaveDebugImage(i, bitmap);
                        
                        results.Add(new Image
                        {
                            Source = bitmap,
                        });
                    }
                }
                return results;
            }            
        }
        private void SaveDebugImage(uint index, BitmapSource bitmap)
        {
            byte[] imageData;
            using(var bmpStream = new MemoryStream())
            {
                BitmapEncoder enc = new BmpBitmapEncoder();
                enc.Frames.Add(BitmapFrame.Create(bitmap));
                enc.Save(bmpStream);
                imageData = bmpStream.ToArray();
            }
            using(var fileStream = new FileStream($"sample_{index}_{System.DateTime.Now:yyyyMMddHHmmssfff}.bmp", FileMode.Create))
            {
                fileStream.Write(imageData, 0, imageData.Length);
            }
        }
    }
}
```
* [c# - Convert BitmapSource to MemoryStream - Stack Overflow](https://stackoverflow.com/questions/42272438/convert-bitmapsource-to-memorystream)

I don't know why.
But maybe because the references of images are shared, so I re-create "BitmapFrame" when I set them as source of "Image".

#### PdfLoader.cs
```cs
...
    BitmapFrame? bitmap = BitmapDecoder.Create(outputStream.AsStream(),
            BitmapCreateOptions.None,    
            BitmapCacheOption.OnLoad).Frames[0];
...    
    results.Add(new Image
    {
        Source = BitmapFrame.Create(bitmap),
    });
...
```
#### Result
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5qid7q14ikf00ptd05em.png)