Використовуємо HTML і веб-Браузера control в якості UI для звичайних windows-додатків на C#

Як відомо, контрол WebBrowser це просто обгортка над компонентом ActiveX для Internet Explorer. Отже він надає доступ до повноцінного layout-движку з усіма сучасними плюшками. А раз так, то спробуємо (сам не знаю правда навіщо) на його основі зробити користувальницький інтерфейс для звичайного windows-додатки.

Можна, звичайно, було б запустити у цьому ж процесі міні веб-сервер (на HttpListener наприклад) і ловити запити через нього, але це занадто просто, нудно і неспортивно. Спробуємо обійтися без мережевих компонентів, just for fun.

Отже, задача проста — необхідно перехоплювати відправку HTML-форм і виводити новий HTML-документ, згенерований логікою програми в залежності від POST-параметрів.

Перш за все нам знадобиться Windows Forms Application з однією єдиною формою на якій буде один єдиний контрол — Браузера займає весь простір.

Для прикладу малюємо кілька простих сторінок інтерфейсу (з мінімальним вмістом для стислості, а так скрипти і стилі додавати за смаком).

Перша сторінка:

<!DOCTYPE html>
<html>
<head><meta http-equiv="X-UA-Compatible" content="IE=11"></head>
<body>
<form method="post">
<input type="text" name="TEXT1" value="Some text" />
<input type="submit" value="Open page 2" name="page2" />
</form>
</body>
</html>

Друга сторінка:

<!DOCTYPE html>
<html>
<head><meta http-equiv="X-UA-Compatible" content="IE=11"></head>
<body>
<div>%TEXT1%</div>
<form method="post">
<input type="submit" value="Back to page 1" name="page1" />
</form>
</body>
</html>

Примітка: X-UA-Compatible необхідний для коректного відображення деяких стилів, в даному прикладі вони не використовуються, але проблема є. Не буду вдаватися в деталі, але без цього компонент малює сторінки в режимі сумісності з чим то дуже старим, з усіма витікаючими наслідками.

WebBrowser надає кілька подій, серед яких є наприклад Navigating, яке спрацьовує перед тим, як відправити форму або перейти за посиланням.
Практично те що потрібно, але це подія не надає ніякої інформації про post-параметрах. GET-параметри використовувати не вийде оскільки у наших HTML-форм атрибут action відсутня, що призведе до того, що в якості URL завжди буде about:blank і ніякої інформації про GET параметрах не буде.
Для отримання більш докладної інформації про запиті треба підписатися на подію BeforeNavigate2 (детальніше тут) внутрішнього COM-об'єкта браузера, благо він доступний через властивість ActiveXInstance.
Зробити це простіше всього через dynamic, щоб не возитися з оголошенням COM-інтерфейсів:
Оголошуємо делегат:

private delegate void BeforeNavigate2(object pDisp, string url, int Flags, string TargetFrameName, byte[] PostData, string Headers, ref bool Cancel);

Потім підписуємося на подію (в конструкторі форми у WebBrowser властивість ActiveXInstance буде null, бо це краще зробити після того, як вікно завантажиться, тобто в OnLoad наприклад):

((dynamic)веб-браузера.ActiveXInstance).BeforeNavigate2 += new BeforeNavigate2(OnBeforeNavigate2);

Отже, в PostData лежать post-параметри у вигляді рядка, що складається з пар ключ=значення об'єднаних через &. Розділяємо їх і укладаємо в словник:

var parameters = (PostData == null ? string.Empty : Encoding.UTF8.GetString(PostData))
.Split('&')
.Select(x => x.Split('='))
.Where(x => x.Length == 2)
.ToDictionary(x => WebUtility.UrlDecode(x[0]), x => WebUtility.UrlDecode(x[1]));

Крім того, в цьому процесорі краще скасувати дію через параметр Cancel, щоб зайвий раз не потрапляти на about:blank.

Маючи параметри генеруємо текст нової сторінки. Сюди теоретично прикручується будь-н менеджер обробників, вибирає необхідний обробник залежно від параметрів, які з будь-н шаблонів будуть збирати сторінки з шматочків, але для стислості поки для прикладу просто по кнопці page1 відкриємо першу сторінку, по кнопці page2 — другу (імена кнопок в розмітці вказувати обов'язково, інакше з post-параметрів не визначити яку саме кнопку натиснули), а також замінимо всі рядки у круглих дужках на значення параметрів з такими іменами:

private static string Handler(IReadOnlyDictionary<string, string> parameters)
{
// do some useful stuff here 
var newPage = "Not found";
if (parameters.ContainsKey(nameof(page1)))
newPage = page1;
if (parameters.ContainsKey(nameof(page2)))
newPage = page2;
parameters.ToList().ForEach(x => newPage = newPage.Replace("{" + x.Key + "}", x.Value));
return newPage;
}

Отриманий рядок з HTML-текстом записуємо у властивість DocumentText WebBrowser-а.
І тут же отримаємо нескінченний цикл перезавантаження сторінки, оскільки установка цього властивості спровокує новий виклик OnBeforeNavigate2.
Тимчасово відписатися від цієї події не вийде, оскільки викликається воно звідки то з циклу обробки повідомлень вже після того як установка DocumentText повертає управління.
Т. о. необхідно завжди ігнорувати кожен другий виклик обробника, оскільки перше спрацьовування надсилання форми в результаті дій користувача, яке потрібно обробляти, а друге — відображення результату яке обробляти не потрібно. Для простоти будемо на початку обробника OnBeforeNavigate2 перемикати bool змінну.

ignore = !ignore;
if (ignore)
return;


І ось результат:



Це все що необхідно для мінімального програми. Але працювати таким чином буде не все — за рамками залишилося наприклад отримання даних про файли для input type=«file», а також робота з XMLHttpRequest для коректної роботи скриптів зі всякими там ajax.

Вихідний код
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Windows.Forms;

namespace TestHtmlUI
{
internal sealed class MainForm : Form
{
private const string page1 = @"<!DOCTYPE html>
<html>
<head><meta http-equiv=""X-UA-Compatible"" content=""IE=11""></head>
<body>
<form method = ""post"" >
<input type=""text"" name=""TEXT1"" value=""Some text"" />
<input type=""submit"" value=""Open page 2"" name=""" + nameof(page2) + @""" />
</form>
</body>
</html>";

private const string page2 = @"<!DOCTYPE html>
<html>
<head><meta http-equiv=""X-UA-Compatible"" content=""IE=11""></head>
<body>
<div>{TEXT1}</div>
<form method=""post"">
<input type=""submit"" value=""Back to page 1"" name=""" + nameof(page1) + @""" />
</form>
</body>
</html>";

private delegate void BeforeNavigate2(object pDisp, string url, int Flags, string TargetFrameName, byte[] PostData, string Headers, ref bool Cancel);

private readonly Браузера webBrowser = new Браузера { Dock = DockStyle.Fill };
private bool ignore = true;

private MainForm()
{
StartPosition = FormStartPosition.CenterScreen;
Controls.Add(браузера);
Load += (sender, e) => ((dynamic)веб-браузера.ActiveXInstance).BeforeNavigate2 += new BeforeNavigate2(OnBeforeNavigate2);

веб-браузера.DocumentText = Handler(new Dictionary<string, string> { { nameof(page1), string.Empty } });
}

private void OnBeforeNavigate2(object pDisp, string url, int Flags, string TargetFrameName, byte[] PostData, string Headers, ref bool Cancel)
{
ignore = !ignore;
if (ignore)
return;
Cancel = true;

var parameters = (PostData == null ? string.Empty : Encoding.UTF8.GetString(PostData))
.Split('&')
.Select(x => x.Split('='))
.Where(x => x.Length == 2)
.ToDictionary(x => WebUtility.UrlDecode(x[0]), x => WebUtility.UrlDecode(x[1]));
веб-браузера.DocumentText = Handler(parameters);
}

[STAThread]
private static void Main()
{
Application.EnableVisualStyles();
Application.Run(new MainForm());
}

private static string Handler(IReadOnlyDictionary<string, string> parameters)
{
var newPage = "Not found";
if (parameters.ContainsKey(nameof(page1)))
newPage = page1;
if (parameters.ContainsKey(nameof(page2)))
newPage = page2;
// do dome usefull stuff here 
parameters.ToList().ForEach(x => newPage = newPage.Replace("{" + x.Key + "}", x.Value));
return newPage;
}
}
}


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.