【.NET 5】【WPF】Try WebView2
Tags: dotnet, csharp, wpf
Published: 2021-01-22 11:41:17

## Intro
This time, I try drawing the values what I had gotten from a spreadsheet.

* [【.NET 5】【WPF】【ClosedXML】Draw and print spreadsheets 1](https://dev.to/masanori_msl/net-5-wpf-closedxml-draw-and-print-spreadsheets-1-16g6)
* [【.NET 5】【WPF】Try MVVM](https://dev.to/masanori_msl/net-5-wpf-try-mvvm-3m0e)

### Environments
* .NET ver.5.0.102
* WebView2 Runtime 
* Microsoft.Extensions.DependencyInjection ver.5.0.1
* Microsoft.Xaml.Behaviors.Wpf ver.1.1.31
* Newtonsoft.Json ver.12.0.3
* Microsoft.Web.WebView2 ver.1.0.664.37

#### package.json
```json
{
    "dependencies": {
        "ts-loader": "^8.0.14",
        "tsc": "^1.20150623.0",
        "typescript": "^4.1.3",
        "webpack": "^5.16.0",
        "webpack-cli": "^4.4.0"
    }
}
```
## Use DataGrid?
First I tried DataGrid.

* [DataGrid Class (System.Windows.Controls) | Microsoft Docs](https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.datagrid?view=net-5.0)

But I felt it was difficult to resolve all of these conditions.

* Adding or deleting columns and rows dynamically
* Changing columns' width and rows' height dynamically 
* Merging cells
* Drawing borders at specific side of cell
* Changing background color
* Changing texts' color

Though I want to try using DataGrid, I try another way in this time.

## Use WebViiew2
I found I could use HTML and CSS grid to do that.

To draw HTML pages on WPF applications, I can use WebView2.

* [Getting started with WebView2 for WPF apps - Microsoft Edge Development | Microsoft Docs](https://docs.microsoft.com/en-us/microsoft-edge/webview2/gettingstarted/wpf)

To use WebView2, I must install WebView2 Runtime on my PC and Microsoft.Web.WebView2 on my project.

* [Webview2 - Microsoft Edge Developer](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)

Now I can add WebView2.

#### MainWindow.xaml
```xml
<Window x:Class="PdfPrintSample.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:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
        xmlns:local="clr-namespace:PdfPrintSample.Main"
        mc:Ignorable="d"
        Title="{Binding Title}" Height="450" Width="800">
...
    <DockPanel>
        <wv2:WebView2 Name="webView"
                    Source="{DEFAULT_URL}">
        </wv2:WebView2>
    </DockPanel>
</Window>
```

## Show local HTML files
As same as web browsers, WebView2 also can open local HTML files.
One important thing is the path must be absolute.

```xml
...
        <wv2:WebView2 Name="webView"
                    Source="C:\Users\example\OneDrive\Documents\Index.html">
        </wv2:WebView2>
...
```

#### Index.html
```html
<!DOCTYPE html>
<html>
	<head>
		<title>Hello</title>
		<script src="js/mainPage.js"></script>
		<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
	</head>
	<body>
		Hello World!
	</body>
</html>
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/j9rr7ukghpevx8epa6cr.jpg)

Because "Source" of WebView2 can data binding.
So I can set the path from ViewModel class.

#### MainWindow.xaml
```xml
...
    <DockPanel>
        <wv2:WebView2 Name="webView"
                    Source="{Binding WebViewSource}">
        </wv2:WebView2>
    </DockPanel>
</Window>
```
#### MainViewModel.cs
```cs
using System;
using System.IO;
using PdfPrintSample.Spreadsheets;
using PdfPrintSample.Spreadsheets.Values;

namespace PdfPrintSample.Main
{
    public class MainViewModel
    {
        public string WebViewSource { get; set; }
...
        public MainViewModel(ISpreadsheetLoader spreadsheets)
        {
            WebViewSource = Path.Combine(Environment.CurrentDirectory, "WebViews/Index.html");
...
        }
...
    }
}
```
### Copy files into the output directory
I want to copy view files into the output directory automatically.
So I add "Content" in the csproj.

#### PdfPrintSample.csproj
```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>
    <UseWPF>true</UseWPF>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <Content Include="Nlog.config">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
    <Content Include="WebViews/*">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
    <Content Include="WebViews/js/*">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="NLog" Version="4.7.6"/>
    <PackageReference Include="PdfSharpCore" Version="1.2.10"/>
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1"/>
    <PackageReference Include="ClosedXML" Version="0.95.4"/>
    <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.31"/>
    <PackageReference Include="Newtonsoft.Json" Version="12.0.3"/>
    <PackageReference Include="Microsoft.Web.WebView2" Version="1.0.664.37"/>
  </ItemGroup>
</Project>
```
## Call TypeScript(JavaScript) functions
WebView2 can call TypeScript(JavaScript) functions.

#### main.page.ts
```ts
export function load(message: string) {
    alert(message);
}
```
#### MainWindow.xaml.cs
```cs
using System.Windows;
using Microsoft.Web.WebView2.Core;

namespace PdfPrintSample.Main
{
    public partial class MainWindow : Window
    {
        public MainWindow(MainViewModel viewModel)
        {
            DataContext = viewModel;
            InitializeComponent();
            InitializeAsync();
            webView.NavigationCompleted += NavigationCompleted;
        }
        private async void InitializeAsync()
        {
            await webView.EnsureCoreWebView2Async(null);
        }
        private async void NavigationCompleted(object? sender,
            CoreWebView2NavigationCompletedEventArgs args)
        {
            // webView.CoreWebView2 is null before completing the navigation
            await webView.CoreWebView2.ExecuteScriptAsync("Page.load('Hello World!')");
        }
    }
}
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/d1s0ehq1hrr39ghb6554.jpg)

### Send loaded spreadsheet data to TypeScript
1.Load a spreadsheet -> 2.Send the data to view(XAML) -> 3.Send the data to TypeScript

There is nothing special.
But I should know that WebView2 may not be ready yet after the spreadsheet finishes loading.
If so, I must wait for finishing readying.

#### MainViewModel.cs
```cs
using System;
using System.IO;
using PdfPrintSample.Spreadsheets;
using PdfPrintSample.Spreadsheets.Values;

namespace PdfPrintSample.Main
{
    public class MainViewModel
    {
        private readonly ISpreadsheetLoader spreadsheets;
        public string Title { get; set; } = "";
        public string WebViewSource { get; set; }
        public LoadCommand Load { get; set; } = new LoadCommand();

        public Action<Spreadsheets.Values.Worksheet?>? WorksheetLoaded { get; set; }
        
        public MainViewModel(ISpreadsheetLoader spreadsheets)
        {
            WebViewSource = Path.Combine(Environment.CurrentDirectory, "WebViews/Index.html");
            this.spreadsheets = spreadsheets;
            Load.LoadSpreadsheetNeeded += LoadSpreadsheet;
        }
        private void LoadSpreadsheet(LoadSpreadsheetArgs args)
        {
            // Send loaded data to view
            var newWorksheet = spreadsheets.Load(args.FilePath, args.SheetName);
            WorksheetLoaded?.Invoke(newWorksheet);            
        }
    }
}
```
#### MainWindow.xaml.cs
```cs
using System;
using System.Windows;
using Microsoft.Web.WebView2.Core;
using Newtonsoft.Json;

namespace PdfPrintSample.Main
{
    public partial class MainWindow : Window
    {
        private EventHandler<EventArgs>? webViewReadyEvent;
        public MainWindow(MainViewModel viewModel)
        {
            DataContext = viewModel;
            InitializeComponent();
            InitializeAsync();
            viewModel.WorksheetLoaded += SpreadsheetLoadedAsync;
        }
        private async void InitializeAsync()
        {
            await webView.EnsureCoreWebView2Async(null);
        }
        private async void SpreadsheetLoadedAsync(Spreadsheets.Values.Worksheet? worksheet)
        {
            string newWorksheetJson = "{}";
            if(worksheet != null)
            {
                newWorksheetJson = JsonConvert.SerializeObject(worksheet);
            }
            if(webView.CoreWebView2 == null)
            {
                webViewReadyEvent = async (sender, args) => {
                    await webView.CoreWebView2!.ExecuteScriptAsync($"Page.load('{newWorksheetJson}')");
                    // remove
                    webView.CoreWebView2Ready -= webViewReadyEvent;                        
                };
                webView.CoreWebView2Ready += webViewReadyEvent;
                return;
            }
            await webView.CoreWebView2!.ExecuteScriptAsync($"Page.load('{newWorksheetJson}')");
        }
    }
}
```